claude-mpm 4.13.2__py3-none-any.whl → 4.18.2__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.
Files changed (250) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_ENGINEER.md +286 -0
  3. claude_mpm/agents/BASE_PM.md +48 -17
  4. claude_mpm/agents/OUTPUT_STYLE.md +329 -11
  5. claude_mpm/agents/PM_INSTRUCTIONS.md +227 -8
  6. claude_mpm/agents/agent_loader.py +17 -5
  7. claude_mpm/agents/frontmatter_validator.py +284 -253
  8. claude_mpm/agents/templates/agentic-coder-optimizer.json +9 -2
  9. claude_mpm/agents/templates/api_qa.json +7 -1
  10. claude_mpm/agents/templates/clerk-ops.json +8 -1
  11. claude_mpm/agents/templates/code_analyzer.json +4 -1
  12. claude_mpm/agents/templates/dart_engineer.json +11 -1
  13. claude_mpm/agents/templates/data_engineer.json +11 -1
  14. claude_mpm/agents/templates/documentation.json +6 -1
  15. claude_mpm/agents/templates/engineer.json +18 -1
  16. claude_mpm/agents/templates/gcp_ops_agent.json +8 -1
  17. claude_mpm/agents/templates/golang_engineer.json +11 -1
  18. claude_mpm/agents/templates/java_engineer.json +12 -2
  19. claude_mpm/agents/templates/local_ops_agent.json +1217 -6
  20. claude_mpm/agents/templates/nextjs_engineer.json +11 -1
  21. claude_mpm/agents/templates/ops.json +8 -1
  22. claude_mpm/agents/templates/php-engineer.json +11 -1
  23. claude_mpm/agents/templates/project_organizer.json +10 -3
  24. claude_mpm/agents/templates/prompt-engineer.json +5 -1
  25. claude_mpm/agents/templates/python_engineer.json +11 -1
  26. claude_mpm/agents/templates/qa.json +7 -1
  27. claude_mpm/agents/templates/react_engineer.json +11 -1
  28. claude_mpm/agents/templates/refactoring_engineer.json +8 -1
  29. claude_mpm/agents/templates/research.json +4 -1
  30. claude_mpm/agents/templates/ruby-engineer.json +11 -1
  31. claude_mpm/agents/templates/rust_engineer.json +11 -1
  32. claude_mpm/agents/templates/security.json +6 -1
  33. claude_mpm/agents/templates/svelte-engineer.json +225 -0
  34. claude_mpm/agents/templates/ticketing.json +6 -1
  35. claude_mpm/agents/templates/typescript_engineer.json +11 -1
  36. claude_mpm/agents/templates/vercel_ops_agent.json +8 -1
  37. claude_mpm/agents/templates/version_control.json +8 -1
  38. claude_mpm/agents/templates/web_qa.json +7 -1
  39. claude_mpm/agents/templates/web_ui.json +11 -1
  40. claude_mpm/cli/__init__.py +34 -706
  41. claude_mpm/cli/commands/agent_manager.py +25 -12
  42. claude_mpm/cli/commands/agent_state_manager.py +186 -0
  43. claude_mpm/cli/commands/agents.py +204 -148
  44. claude_mpm/cli/commands/aggregate.py +7 -3
  45. claude_mpm/cli/commands/analyze.py +9 -4
  46. claude_mpm/cli/commands/analyze_code.py +7 -2
  47. claude_mpm/cli/commands/auto_configure.py +7 -9
  48. claude_mpm/cli/commands/config.py +47 -13
  49. claude_mpm/cli/commands/configure.py +294 -1788
  50. claude_mpm/cli/commands/configure_agent_display.py +261 -0
  51. claude_mpm/cli/commands/configure_behavior_manager.py +204 -0
  52. claude_mpm/cli/commands/configure_hook_manager.py +225 -0
  53. claude_mpm/cli/commands/configure_models.py +18 -0
  54. claude_mpm/cli/commands/configure_navigation.py +167 -0
  55. claude_mpm/cli/commands/configure_paths.py +104 -0
  56. claude_mpm/cli/commands/configure_persistence.py +254 -0
  57. claude_mpm/cli/commands/configure_startup_manager.py +646 -0
  58. claude_mpm/cli/commands/configure_template_editor.py +497 -0
  59. claude_mpm/cli/commands/configure_validators.py +73 -0
  60. claude_mpm/cli/commands/local_deploy.py +537 -0
  61. claude_mpm/cli/commands/memory.py +54 -20
  62. claude_mpm/cli/commands/mpm_init.py +39 -25
  63. claude_mpm/cli/commands/mpm_init_handler.py +8 -3
  64. claude_mpm/cli/executor.py +202 -0
  65. claude_mpm/cli/helpers.py +105 -0
  66. claude_mpm/cli/interactive/__init__.py +3 -0
  67. claude_mpm/cli/interactive/skills_wizard.py +491 -0
  68. claude_mpm/cli/parsers/__init__.py +7 -1
  69. claude_mpm/cli/parsers/base_parser.py +98 -3
  70. claude_mpm/cli/parsers/local_deploy_parser.py +227 -0
  71. claude_mpm/cli/shared/output_formatters.py +28 -19
  72. claude_mpm/cli/startup.py +481 -0
  73. claude_mpm/cli/utils.py +52 -1
  74. claude_mpm/commands/mpm-help.md +3 -0
  75. claude_mpm/commands/mpm-version.md +113 -0
  76. claude_mpm/commands/mpm.md +1 -0
  77. claude_mpm/config/agent_config.py +2 -2
  78. claude_mpm/config/model_config.py +428 -0
  79. claude_mpm/core/base_service.py +13 -12
  80. claude_mpm/core/enums.py +452 -0
  81. claude_mpm/core/factories.py +1 -1
  82. claude_mpm/core/instruction_reinforcement_hook.py +2 -1
  83. claude_mpm/core/interactive_session.py +9 -3
  84. claude_mpm/core/logging_config.py +6 -2
  85. claude_mpm/core/oneshot_session.py +8 -4
  86. claude_mpm/core/optimized_agent_loader.py +3 -3
  87. claude_mpm/core/output_style_manager.py +12 -192
  88. claude_mpm/core/service_registry.py +5 -1
  89. claude_mpm/core/types.py +2 -9
  90. claude_mpm/core/typing_utils.py +7 -6
  91. claude_mpm/dashboard/static/js/dashboard.js +0 -14
  92. claude_mpm/dashboard/templates/index.html +3 -41
  93. claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
  94. claude_mpm/hooks/instruction_reinforcement.py +7 -2
  95. claude_mpm/models/resume_log.py +340 -0
  96. claude_mpm/services/agents/auto_config_manager.py +10 -11
  97. claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
  98. claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
  99. claude_mpm/services/agents/deployment/agent_validator.py +17 -1
  100. claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
  101. claude_mpm/services/agents/deployment/interface_adapter.py +3 -2
  102. claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
  103. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +7 -6
  104. claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +7 -16
  105. claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +4 -3
  106. claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +5 -3
  107. claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +6 -5
  108. claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +9 -6
  109. claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
  110. claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
  111. claude_mpm/services/agents/local_template_manager.py +1 -1
  112. claude_mpm/services/agents/memory/agent_memory_manager.py +5 -2
  113. claude_mpm/services/agents/registry/modification_tracker.py +5 -2
  114. claude_mpm/services/command_handler_service.py +11 -5
  115. claude_mpm/services/core/interfaces/__init__.py +74 -2
  116. claude_mpm/services/core/interfaces/health.py +172 -0
  117. claude_mpm/services/core/interfaces/model.py +281 -0
  118. claude_mpm/services/core/interfaces/process.py +372 -0
  119. claude_mpm/services/core/interfaces/restart.py +307 -0
  120. claude_mpm/services/core/interfaces/stability.py +260 -0
  121. claude_mpm/services/core/models/__init__.py +33 -0
  122. claude_mpm/services/core/models/agent_config.py +12 -28
  123. claude_mpm/services/core/models/health.py +162 -0
  124. claude_mpm/services/core/models/process.py +235 -0
  125. claude_mpm/services/core/models/restart.py +302 -0
  126. claude_mpm/services/core/models/stability.py +264 -0
  127. claude_mpm/services/core/path_resolver.py +23 -7
  128. claude_mpm/services/diagnostics/__init__.py +2 -2
  129. claude_mpm/services/diagnostics/checks/agent_check.py +25 -24
  130. claude_mpm/services/diagnostics/checks/claude_code_check.py +24 -23
  131. claude_mpm/services/diagnostics/checks/common_issues_check.py +25 -24
  132. claude_mpm/services/diagnostics/checks/configuration_check.py +24 -23
  133. claude_mpm/services/diagnostics/checks/filesystem_check.py +18 -17
  134. claude_mpm/services/diagnostics/checks/installation_check.py +30 -29
  135. claude_mpm/services/diagnostics/checks/instructions_check.py +20 -19
  136. claude_mpm/services/diagnostics/checks/mcp_check.py +50 -36
  137. claude_mpm/services/diagnostics/checks/mcp_services_check.py +36 -31
  138. claude_mpm/services/diagnostics/checks/monitor_check.py +23 -22
  139. claude_mpm/services/diagnostics/checks/startup_log_check.py +9 -8
  140. claude_mpm/services/diagnostics/diagnostic_runner.py +6 -5
  141. claude_mpm/services/diagnostics/doctor_reporter.py +28 -25
  142. claude_mpm/services/diagnostics/models.py +19 -24
  143. claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
  144. claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
  145. claude_mpm/services/infrastructure/monitoring/base.py +5 -13
  146. claude_mpm/services/infrastructure/monitoring/network.py +7 -6
  147. claude_mpm/services/infrastructure/monitoring/process.py +13 -12
  148. claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
  149. claude_mpm/services/infrastructure/monitoring/service.py +16 -15
  150. claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
  151. claude_mpm/services/local_ops/__init__.py +163 -0
  152. claude_mpm/services/local_ops/crash_detector.py +257 -0
  153. claude_mpm/services/local_ops/health_checks/__init__.py +28 -0
  154. claude_mpm/services/local_ops/health_checks/http_check.py +224 -0
  155. claude_mpm/services/local_ops/health_checks/process_check.py +236 -0
  156. claude_mpm/services/local_ops/health_checks/resource_check.py +255 -0
  157. claude_mpm/services/local_ops/health_manager.py +430 -0
  158. claude_mpm/services/local_ops/log_monitor.py +396 -0
  159. claude_mpm/services/local_ops/memory_leak_detector.py +294 -0
  160. claude_mpm/services/local_ops/process_manager.py +595 -0
  161. claude_mpm/services/local_ops/resource_monitor.py +331 -0
  162. claude_mpm/services/local_ops/restart_manager.py +401 -0
  163. claude_mpm/services/local_ops/restart_policy.py +387 -0
  164. claude_mpm/services/local_ops/state_manager.py +372 -0
  165. claude_mpm/services/local_ops/unified_manager.py +600 -0
  166. claude_mpm/services/mcp_config_manager.py +9 -4
  167. claude_mpm/services/mcp_gateway/core/__init__.py +1 -2
  168. claude_mpm/services/mcp_gateway/core/base.py +18 -31
  169. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +71 -24
  170. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +30 -28
  171. claude_mpm/services/memory_hook_service.py +4 -1
  172. claude_mpm/services/model/__init__.py +147 -0
  173. claude_mpm/services/model/base_provider.py +365 -0
  174. claude_mpm/services/model/claude_provider.py +412 -0
  175. claude_mpm/services/model/model_router.py +453 -0
  176. claude_mpm/services/model/ollama_provider.py +415 -0
  177. claude_mpm/services/monitor/daemon_manager.py +3 -2
  178. claude_mpm/services/monitor/handlers/dashboard.py +2 -1
  179. claude_mpm/services/monitor/handlers/hooks.py +2 -1
  180. claude_mpm/services/monitor/management/lifecycle.py +3 -2
  181. claude_mpm/services/monitor/server.py +2 -1
  182. claude_mpm/services/session_management_service.py +3 -2
  183. claude_mpm/services/session_manager.py +205 -1
  184. claude_mpm/services/shared/async_service_base.py +16 -27
  185. claude_mpm/services/shared/lifecycle_service_base.py +1 -14
  186. claude_mpm/services/socketio/handlers/__init__.py +5 -2
  187. claude_mpm/services/socketio/handlers/hook.py +13 -2
  188. claude_mpm/services/socketio/handlers/registry.py +4 -2
  189. claude_mpm/services/socketio/server/main.py +10 -8
  190. claude_mpm/services/subprocess_launcher_service.py +14 -5
  191. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +8 -7
  192. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +6 -5
  193. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +8 -7
  194. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +7 -6
  195. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +5 -4
  196. claude_mpm/services/unified/config_strategies/validation_strategy.py +13 -9
  197. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +10 -3
  198. claude_mpm/services/unified/deployment_strategies/local.py +6 -5
  199. claude_mpm/services/unified/deployment_strategies/utils.py +6 -5
  200. claude_mpm/services/unified/deployment_strategies/vercel.py +7 -6
  201. claude_mpm/services/unified/interfaces.py +3 -1
  202. claude_mpm/services/unified/unified_analyzer.py +14 -10
  203. claude_mpm/services/unified/unified_config.py +2 -1
  204. claude_mpm/services/unified/unified_deployment.py +9 -4
  205. claude_mpm/services/version_service.py +104 -1
  206. claude_mpm/skills/__init__.py +21 -0
  207. claude_mpm/skills/bundled/__init__.py +6 -0
  208. claude_mpm/skills/bundled/api-documentation.md +393 -0
  209. claude_mpm/skills/bundled/async-testing.md +571 -0
  210. claude_mpm/skills/bundled/code-review.md +143 -0
  211. claude_mpm/skills/bundled/database-migration.md +199 -0
  212. claude_mpm/skills/bundled/docker-containerization.md +194 -0
  213. claude_mpm/skills/bundled/express-local-dev.md +1429 -0
  214. claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
  215. claude_mpm/skills/bundled/git-workflow.md +414 -0
  216. claude_mpm/skills/bundled/imagemagick.md +204 -0
  217. claude_mpm/skills/bundled/json-data-handling.md +223 -0
  218. claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
  219. claude_mpm/skills/bundled/pdf.md +141 -0
  220. claude_mpm/skills/bundled/performance-profiling.md +567 -0
  221. claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
  222. claude_mpm/skills/bundled/security-scanning.md +327 -0
  223. claude_mpm/skills/bundled/systematic-debugging.md +473 -0
  224. claude_mpm/skills/bundled/test-driven-development.md +378 -0
  225. claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
  226. claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
  227. claude_mpm/skills/bundled/xlsx.md +157 -0
  228. claude_mpm/skills/registry.py +286 -0
  229. claude_mpm/skills/skill_manager.py +310 -0
  230. claude_mpm/tools/code_tree_analyzer.py +177 -141
  231. claude_mpm/tools/code_tree_events.py +4 -2
  232. claude_mpm/utils/agent_dependency_loader.py +2 -2
  233. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/METADATA +117 -8
  234. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/RECORD +238 -174
  235. claude_mpm/dashboard/static/css/code-tree.css +0 -1639
  236. claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
  237. claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
  238. claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
  239. claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
  240. claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
  241. claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
  242. claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +0 -425
  243. claude_mpm/hooks/claude_hooks/hook_handler_original.py +0 -1041
  244. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +0 -347
  245. claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +0 -575
  246. claude_mpm/services/project/analyzer_refactored.py +0 -450
  247. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/WHEEL +0 -0
  248. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/entry_points.txt +0 -0
  249. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/licenses/LICENSE +0 -0
  250. {claude_mpm-4.13.2.dist-info → claude_mpm-4.18.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,372 @@
1
+ """
2
+ Deployment State Manager for Claude MPM Framework
3
+ =================================================
4
+
5
+ WHY: Provides persistent state tracking for local deployments with atomic
6
+ operations, file locking, and corruption recovery. Critical for preventing
7
+ orphaned processes and ensuring deployment reliability.
8
+
9
+ DESIGN DECISION: Uses JSON file storage with filelock for simplicity and
10
+ portability. File-based storage is sufficient for local deployments and
11
+ doesn't require external dependencies.
12
+
13
+ ARCHITECTURE:
14
+ - Thread-safe operations with file locking
15
+ - Atomic read-modify-write cycles
16
+ - Automatic corruption detection and recovery
17
+ - Process validation using psutil
18
+
19
+ USAGE:
20
+ manager = DeploymentStateManager(state_file_path)
21
+ manager.add_deployment(deployment_state)
22
+ deployments = manager.get_all_deployments()
23
+ manager.cleanup_dead_pids()
24
+ """
25
+
26
+ import json
27
+ from pathlib import Path
28
+ from typing import Dict, List, Optional
29
+
30
+ import psutil
31
+ from filelock import FileLock
32
+
33
+ from claude_mpm.core.enums import ServiceState
34
+ from claude_mpm.services.core.base import SyncBaseService
35
+ from claude_mpm.services.core.interfaces.process import IDeploymentStateManager
36
+ from claude_mpm.services.core.models.process import DeploymentState
37
+
38
+
39
+ class StateCorruptionError(Exception):
40
+ """Raised when state file is corrupted and cannot be recovered."""
41
+
42
+
43
+ class DeploymentStateManager(SyncBaseService, IDeploymentStateManager):
44
+ """
45
+ Manages persistent deployment state with atomic operations.
46
+
47
+ WHY: Deployment state must survive restarts and be accessible to
48
+ multiple processes. This manager ensures consistency with file locking
49
+ and provides corruption recovery.
50
+
51
+ Thread Safety: All public methods use file locking for atomicity.
52
+ """
53
+
54
+ def __init__(self, state_file_path: str):
55
+ """
56
+ Initialize state manager.
57
+
58
+ Args:
59
+ state_file_path: Path to JSON state file
60
+
61
+ Raises:
62
+ ValueError: If state_file_path is invalid
63
+ """
64
+ super().__init__("DeploymentStateManager")
65
+
66
+ self.state_file = Path(state_file_path)
67
+ self.lock_file = Path(str(state_file_path) + ".lock")
68
+
69
+ # Create single FileLock instance for re-entrant locking
70
+ # WHY: Using the same lock instance allows re-entrant calls
71
+ # (e.g., add_deployment -> load_state) without deadlock
72
+ self._file_lock = FileLock(str(self.lock_file), timeout=10)
73
+
74
+ # Ensure parent directory exists
75
+ self.state_file.parent.mkdir(parents=True, exist_ok=True)
76
+
77
+ # Initialize empty state if file doesn't exist
78
+ if not self.state_file.exists():
79
+ self._write_state({})
80
+
81
+ self.log_info(f"Initialized state manager with file: {self.state_file}")
82
+
83
+ def initialize(self) -> bool:
84
+ """
85
+ Initialize the state manager.
86
+
87
+ Returns:
88
+ True if initialization successful
89
+ """
90
+ try:
91
+ # Validate state file can be read
92
+ self.load_state()
93
+ self._initialized = True
94
+ return True
95
+ except Exception as e:
96
+ self.log_error(f"Failed to initialize: {e}")
97
+ return False
98
+
99
+ def shutdown(self) -> None:
100
+ """Shutdown state manager (no resources to clean up)."""
101
+ self._shutdown = True
102
+ self.log_info("State manager shutdown complete")
103
+
104
+ def load_state(self) -> Dict[str, DeploymentState]:
105
+ """
106
+ Load all deployment states from file.
107
+
108
+ Returns:
109
+ Dictionary mapping deployment_id to DeploymentState
110
+
111
+ Raises:
112
+ StateCorruptionError: If state file is corrupted beyond recovery
113
+ """
114
+ with self._file_lock:
115
+ try:
116
+ if not self.state_file.exists():
117
+ return {}
118
+
119
+ with self.state_file.open() as f:
120
+ data = json.load(f)
121
+
122
+ # Convert dict entries to DeploymentState objects
123
+ states = {}
124
+ for deployment_id, state_dict in data.items():
125
+ try:
126
+ states[deployment_id] = DeploymentState.from_dict(state_dict)
127
+ except Exception as e:
128
+ self.log_warning(
129
+ f"Skipping corrupted state entry {deployment_id}: {e}"
130
+ )
131
+
132
+ return states
133
+
134
+ except json.JSONDecodeError as e:
135
+ self.log_error(f"State file corrupted: {e}")
136
+ # Attempt recovery by backing up and creating fresh state
137
+ backup_path = self.state_file.with_suffix(".json.corrupted")
138
+ self.state_file.rename(backup_path)
139
+ self.log_warning(f"Backed up corrupted state to {backup_path}")
140
+ self._write_state({})
141
+ return {}
142
+
143
+ except Exception as e:
144
+ raise StateCorruptionError(f"Failed to load state: {e}") from e
145
+
146
+ def save_state(self, states: Dict[str, DeploymentState]) -> None:
147
+ """
148
+ Save all deployment states to file.
149
+
150
+ Args:
151
+ states: Dictionary mapping deployment_id to DeploymentState
152
+
153
+ Raises:
154
+ IOError: If state file cannot be written
155
+ """
156
+ with self._file_lock:
157
+ self._write_state(states)
158
+
159
+ def _write_state(self, states: Dict[str, DeploymentState]) -> None:
160
+ """
161
+ Internal method to write state without locking.
162
+
163
+ WHY: Allows caller to handle locking for atomic operations.
164
+
165
+ Args:
166
+ states: States to write (can be dict or DeploymentState dict)
167
+ """
168
+ # Convert DeploymentState objects to dicts
169
+ data = {}
170
+ for deployment_id, state in states.items():
171
+ if isinstance(state, DeploymentState):
172
+ data[deployment_id] = state.to_dict()
173
+ else:
174
+ data[deployment_id] = state
175
+
176
+ # Atomic write: write to temp file then rename
177
+ temp_file = self.state_file.with_suffix(".tmp")
178
+ try:
179
+ with temp_file.open("w") as f:
180
+ json.dump(data, f, indent=2)
181
+ temp_file.replace(self.state_file)
182
+ except Exception as e:
183
+ if temp_file.exists():
184
+ temp_file.unlink()
185
+ raise OSError(f"Failed to write state: {e}") from e
186
+
187
+ def get_deployment(self, deployment_id: str) -> Optional[DeploymentState]:
188
+ """
189
+ Get a specific deployment by ID.
190
+
191
+ Args:
192
+ deployment_id: Unique deployment identifier
193
+
194
+ Returns:
195
+ DeploymentState if found, None otherwise
196
+ """
197
+ states = self.load_state()
198
+ return states.get(deployment_id)
199
+
200
+ def get_all_deployments(self) -> List[DeploymentState]:
201
+ """
202
+ Get all tracked deployments.
203
+
204
+ Returns:
205
+ List of all DeploymentState objects
206
+ """
207
+ states = self.load_state()
208
+ return list(states.values())
209
+
210
+ def get_deployments_by_status(self, status: ServiceState) -> List[DeploymentState]:
211
+ """
212
+ Get all deployments with a specific status.
213
+
214
+ Args:
215
+ status: ServiceState to filter by
216
+
217
+ Returns:
218
+ List of matching DeploymentState objects
219
+ """
220
+ states = self.load_state()
221
+ return [s for s in states.values() if s.status == status]
222
+
223
+ def get_deployment_by_port(self, port: int) -> Optional[DeploymentState]:
224
+ """
225
+ Get deployment using a specific port.
226
+
227
+ Args:
228
+ port: Port number to search for
229
+
230
+ Returns:
231
+ DeploymentState if found, None otherwise
232
+ """
233
+ states = self.load_state()
234
+ for state in states.values():
235
+ if state.port == port:
236
+ return state
237
+ return None
238
+
239
+ def get_deployments_by_project(
240
+ self, working_directory: str
241
+ ) -> List[DeploymentState]:
242
+ """
243
+ Get all deployments for a specific project directory.
244
+
245
+ Args:
246
+ working_directory: Project directory path
247
+
248
+ Returns:
249
+ List of matching DeploymentState objects
250
+ """
251
+ # Normalize path for comparison
252
+ normalized_dir = str(Path(working_directory).absolute())
253
+ states = self.load_state()
254
+ return [
255
+ s
256
+ for s in states.values()
257
+ if str(Path(s.working_directory).absolute()) == normalized_dir
258
+ ]
259
+
260
+ def add_deployment(self, deployment: DeploymentState) -> None:
261
+ """
262
+ Add or update a deployment in state.
263
+
264
+ Args:
265
+ deployment: DeploymentState to add/update
266
+
267
+ Raises:
268
+ IOError: If state cannot be persisted
269
+ """
270
+ with self._file_lock:
271
+ states = self.load_state()
272
+ states[deployment.deployment_id] = deployment
273
+ self._write_state(states)
274
+ self.log_debug(f"Added deployment: {deployment.deployment_id}")
275
+
276
+ def remove_deployment(self, deployment_id: str) -> bool:
277
+ """
278
+ Remove a deployment from state.
279
+
280
+ Args:
281
+ deployment_id: Unique deployment identifier
282
+
283
+ Returns:
284
+ True if deployment was removed, False if not found
285
+
286
+ Raises:
287
+ IOError: If state cannot be persisted
288
+ """
289
+ with self._file_lock:
290
+ states = self.load_state()
291
+ if deployment_id in states:
292
+ del states[deployment_id]
293
+ self._write_state(states)
294
+ self.log_debug(f"Removed deployment: {deployment_id}")
295
+ return True
296
+ return False
297
+
298
+ def update_deployment_status(
299
+ self, deployment_id: str, status: ServiceState
300
+ ) -> bool:
301
+ """
302
+ Update the status of a deployment.
303
+
304
+ Args:
305
+ deployment_id: Unique deployment identifier
306
+ status: New ServiceState
307
+
308
+ Returns:
309
+ True if updated, False if deployment not found
310
+
311
+ Raises:
312
+ IOError: If state cannot be persisted
313
+ """
314
+ with self._file_lock:
315
+ states = self.load_state()
316
+ if deployment_id in states:
317
+ states[deployment_id].status = status
318
+ self._write_state(states)
319
+ self.log_debug(f"Updated status for {deployment_id}: {status.value}")
320
+ return True
321
+ return False
322
+
323
+ def cleanup_dead_pids(self) -> int:
324
+ """
325
+ Remove deployments with dead process IDs.
326
+
327
+ WHY: Processes may crash or be killed externally. This method cleans
328
+ up stale state entries for processes that no longer exist.
329
+
330
+ Returns:
331
+ Number of dead PIDs cleaned up
332
+
333
+ Raises:
334
+ IOError: If state cannot be persisted
335
+ """
336
+ with self._file_lock:
337
+ states = self.load_state()
338
+ cleaned_count = 0
339
+
340
+ for deployment_id, state in list(states.items()):
341
+ if not self._is_pid_alive(state.process_id):
342
+ self.log_info(
343
+ f"Cleaning dead PID {state.process_id} for {deployment_id}"
344
+ )
345
+ del states[deployment_id]
346
+ cleaned_count += 1
347
+
348
+ if cleaned_count > 0:
349
+ self._write_state(states)
350
+ self.log_info(f"Cleaned up {cleaned_count} dead PIDs")
351
+
352
+ return cleaned_count
353
+
354
+ def _is_pid_alive(self, pid: int) -> bool:
355
+ """
356
+ Check if a process ID is alive.
357
+
358
+ Args:
359
+ pid: Process ID to check
360
+
361
+ Returns:
362
+ True if process exists and is running
363
+ """
364
+ try:
365
+ process = psutil.Process(pid)
366
+ # Check if process still exists and is not a zombie
367
+ return process.is_running() and process.status() != psutil.STATUS_ZOMBIE
368
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
369
+ return False
370
+
371
+
372
+ __all__ = ["DeploymentStateManager", "StateCorruptionError"]