claude-mpm 3.5.6__py3-none-any.whl → 3.6.0__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 (46) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +96 -23
  3. claude_mpm/agents/BASE_PM.md +273 -0
  4. claude_mpm/agents/INSTRUCTIONS.md +114 -103
  5. claude_mpm/agents/agent_loader.py +36 -1
  6. claude_mpm/agents/async_agent_loader.py +421 -0
  7. claude_mpm/agents/templates/code_analyzer.json +81 -0
  8. claude_mpm/agents/templates/data_engineer.json +18 -3
  9. claude_mpm/agents/templates/documentation.json +18 -3
  10. claude_mpm/agents/templates/engineer.json +19 -4
  11. claude_mpm/agents/templates/ops.json +18 -3
  12. claude_mpm/agents/templates/qa.json +20 -4
  13. claude_mpm/agents/templates/research.json +20 -4
  14. claude_mpm/agents/templates/security.json +18 -3
  15. claude_mpm/agents/templates/version_control.json +16 -3
  16. claude_mpm/cli/__init__.py +5 -1
  17. claude_mpm/cli/commands/__init__.py +5 -1
  18. claude_mpm/cli/commands/agents.py +212 -3
  19. claude_mpm/cli/commands/aggregate.py +462 -0
  20. claude_mpm/cli/commands/config.py +277 -0
  21. claude_mpm/cli/commands/run.py +224 -36
  22. claude_mpm/cli/parser.py +176 -1
  23. claude_mpm/constants.py +19 -0
  24. claude_mpm/core/claude_runner.py +320 -44
  25. claude_mpm/core/config.py +161 -4
  26. claude_mpm/core/framework_loader.py +81 -0
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
  28. claude_mpm/init.py +40 -5
  29. claude_mpm/models/agent_session.py +511 -0
  30. claude_mpm/scripts/__init__.py +15 -0
  31. claude_mpm/scripts/start_activity_logging.py +86 -0
  32. claude_mpm/services/agents/deployment/agent_deployment.py +165 -19
  33. claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
  34. claude_mpm/services/event_aggregator.py +547 -0
  35. claude_mpm/utils/agent_dependency_loader.py +655 -0
  36. claude_mpm/utils/console.py +11 -0
  37. claude_mpm/utils/dependency_cache.py +376 -0
  38. claude_mpm/utils/dependency_strategies.py +343 -0
  39. claude_mpm/utils/environment_context.py +310 -0
  40. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/METADATA +47 -3
  41. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/RECORD +45 -31
  42. claude_mpm/agents/templates/pm.json +0 -122
  43. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/WHEEL +0 -0
  44. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/entry_points.txt +0 -0
  45. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/licenses/LICENSE +0 -0
  46. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,343 @@
1
+ """
2
+ Dependency management strategies for different contexts.
3
+
4
+ This module provides smart dependency checking and installation strategies
5
+ based on the execution context and user preferences.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import time
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Optional, Dict, Any, Tuple
14
+ from enum import Enum
15
+ from datetime import datetime, timedelta
16
+
17
+ from ..core.logger import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class DependencyMode(Enum):
23
+ """Dependency checking and installation modes."""
24
+ OFF = "off" # No checking at all
25
+ CHECK = "check" # Check and warn only
26
+ INTERACTIVE = "interactive" # Prompt user for installation
27
+ AUTO = "auto" # Automatically install missing deps
28
+ LAZY = "lazy" # Check only when agent is invoked
29
+
30
+
31
+ class DependencyStrategy:
32
+ """
33
+ Smart dependency management based on context and preferences.
34
+
35
+ This class determines the appropriate dependency strategy based on:
36
+ - Execution environment (CI, Docker, TTY, etc.)
37
+ - User configuration
38
+ - Cached check results
39
+ - Command being executed
40
+ """
41
+
42
+ def __init__(self, config_path: Optional[Path] = None):
43
+ """
44
+ Initialize dependency strategy manager.
45
+
46
+ Args:
47
+ config_path: Optional path to configuration file
48
+ """
49
+ self.config_path = config_path or Path.home() / ".claude-mpm" / "config.yaml"
50
+ self.cache_path = Path.home() / ".claude-mpm" / ".dep_cache.json"
51
+ self.mode = self._determine_mode()
52
+
53
+ def _determine_mode(self) -> DependencyMode:
54
+ """
55
+ Determine the appropriate dependency mode based on context.
56
+
57
+ Returns:
58
+ The dependency mode to use
59
+ """
60
+ # Check environment variable override
61
+ env_mode = os.environ.get("CLAUDE_MPM_DEP_MODE")
62
+ if env_mode:
63
+ try:
64
+ return DependencyMode(env_mode.lower())
65
+ except ValueError:
66
+ logger.warning(f"Invalid CLAUDE_MPM_DEP_MODE: {env_mode}")
67
+
68
+ # Check if in CI environment
69
+ if self._is_ci_environment():
70
+ logger.debug("CI environment detected - using CHECK mode")
71
+ return DependencyMode.CHECK
72
+
73
+ # Check if in Docker container
74
+ if self._is_docker():
75
+ logger.debug("Docker environment detected - using CHECK mode")
76
+ return DependencyMode.CHECK
77
+
78
+ # Check if non-interactive terminal
79
+ if not self._is_interactive():
80
+ logger.debug("Non-interactive terminal - using CHECK mode")
81
+ return DependencyMode.CHECK
82
+
83
+ # Load user configuration
84
+ user_mode = self._load_user_preference()
85
+ if user_mode:
86
+ return user_mode
87
+
88
+ # Default to interactive for TTY sessions
89
+ return DependencyMode.INTERACTIVE
90
+
91
+ def _is_ci_environment(self) -> bool:
92
+ """Check if running in CI environment."""
93
+ ci_indicators = [
94
+ "CI", "CONTINUOUS_INTEGRATION", "JENKINS", "TRAVIS",
95
+ "CIRCLECI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE"
96
+ ]
97
+ return any(os.environ.get(var) for var in ci_indicators)
98
+
99
+ def _is_docker(self) -> bool:
100
+ """Check if running inside Docker container."""
101
+ return (
102
+ os.path.exists("/.dockerenv") or
103
+ os.environ.get("KUBERNETES_SERVICE_HOST") is not None
104
+ )
105
+
106
+ def _is_interactive(self) -> bool:
107
+ """Check if running in interactive terminal."""
108
+ return sys.stdin.isatty() and sys.stdout.isatty()
109
+
110
+ def _load_user_preference(self) -> Optional[DependencyMode]:
111
+ """
112
+ Load user preference from config file.
113
+
114
+ Returns:
115
+ User's preferred dependency mode or None
116
+ """
117
+ if not self.config_path.exists():
118
+ return None
119
+
120
+ try:
121
+ # Try to load YAML config
122
+ import yaml
123
+ with open(self.config_path) as f:
124
+ config = yaml.safe_load(f)
125
+ mode_str = config.get("dependency_mode")
126
+ if mode_str:
127
+ return DependencyMode(mode_str)
128
+ except Exception as e:
129
+ logger.debug(f"Could not load config: {e}")
130
+
131
+ return None
132
+
133
+ def should_check_now(self, cache_ttl: int = 86400) -> bool:
134
+ """
135
+ Determine if dependency check should run now based on cache.
136
+
137
+ Args:
138
+ cache_ttl: Cache time-to-live in seconds (default 24 hours)
139
+
140
+ Returns:
141
+ True if check should run, False if cached results are fresh
142
+ """
143
+ if self.mode == DependencyMode.OFF:
144
+ return False
145
+
146
+ if not self.cache_path.exists():
147
+ return True
148
+
149
+ try:
150
+ with open(self.cache_path) as f:
151
+ cache = json.load(f)
152
+ last_check = datetime.fromisoformat(cache.get("timestamp", ""))
153
+
154
+ # Check if cache is still valid
155
+ if datetime.now() - last_check < timedelta(seconds=cache_ttl):
156
+ logger.debug(f"Using cached dependency check from {last_check}")
157
+ return False
158
+
159
+ except Exception as e:
160
+ logger.debug(f"Cache invalid or corrupted: {e}")
161
+
162
+ return True
163
+
164
+ def cache_results(self, results: Dict[str, Any]) -> None:
165
+ """
166
+ Cache dependency check results.
167
+
168
+ Args:
169
+ results: Dependency check results to cache
170
+ """
171
+ try:
172
+ self.cache_path.parent.mkdir(parents=True, exist_ok=True)
173
+
174
+ cache_data = {
175
+ "timestamp": datetime.now().isoformat(),
176
+ "results": results
177
+ }
178
+
179
+ with open(self.cache_path, 'w') as f:
180
+ json.dump(cache_data, f, indent=2)
181
+
182
+ logger.debug(f"Cached dependency results to {self.cache_path}")
183
+
184
+ except Exception as e:
185
+ logger.warning(f"Failed to cache results: {e}")
186
+
187
+ def get_cached_results(self) -> Optional[Dict[str, Any]]:
188
+ """
189
+ Get cached dependency check results if available.
190
+
191
+ Returns:
192
+ Cached results or None
193
+ """
194
+ if not self.cache_path.exists():
195
+ return None
196
+
197
+ try:
198
+ with open(self.cache_path) as f:
199
+ cache = json.load(f)
200
+ return cache.get("results")
201
+ except Exception:
202
+ return None
203
+
204
+ def prompt_for_installation(self, missing_deps: list) -> str:
205
+ """
206
+ Prompt user for dependency installation preference.
207
+
208
+ Args:
209
+ missing_deps: List of missing dependencies
210
+
211
+ Returns:
212
+ User's choice: 'yes', 'no', 'always', 'never'
213
+ """
214
+ if not self._is_interactive():
215
+ return 'no'
216
+
217
+ print(f"\n⚠️ Missing {len(missing_deps)} dependencies:")
218
+ for dep in missing_deps[:5]: # Show first 5
219
+ print(f" - {dep}")
220
+ if len(missing_deps) > 5:
221
+ print(f" ... and {len(missing_deps) - 5} more")
222
+
223
+ while True:
224
+ response = input("\nInstall missing dependencies? [y/N/always/never]: ").lower().strip()
225
+
226
+ if response in ['y', 'yes']:
227
+ return 'yes'
228
+ elif response in ['n', 'no', '']:
229
+ return 'no'
230
+ elif response == 'always':
231
+ self._save_preference(DependencyMode.AUTO)
232
+ return 'yes'
233
+ elif response == 'never':
234
+ self._save_preference(DependencyMode.OFF)
235
+ return 'no'
236
+ else:
237
+ print("Invalid choice. Please enter: y, n, always, or never")
238
+
239
+ def _save_preference(self, mode: DependencyMode) -> None:
240
+ """
241
+ Save user's dependency mode preference.
242
+
243
+ Args:
244
+ mode: The dependency mode to save
245
+ """
246
+ try:
247
+ self.config_path.parent.mkdir(parents=True, exist_ok=True)
248
+
249
+ # Load existing config or create new
250
+ config = {}
251
+ if self.config_path.exists():
252
+ import yaml
253
+ with open(self.config_path) as f:
254
+ config = yaml.safe_load(f) or {}
255
+
256
+ # Update dependency mode
257
+ config['dependency_mode'] = mode.value
258
+
259
+ # Save config
260
+ import yaml
261
+ with open(self.config_path, 'w') as f:
262
+ yaml.dump(config, f, default_flow_style=False)
263
+
264
+ print(f"✓ Saved preference: {mode.value}")
265
+
266
+ except Exception as e:
267
+ logger.error(f"Failed to save preference: {e}")
268
+
269
+
270
+ def get_smart_dependency_handler(command: Optional[str] = None) -> Tuple[DependencyMode, DependencyStrategy]:
271
+ """
272
+ Get the appropriate dependency handler for the current context.
273
+
274
+ Args:
275
+ command: The command being executed (e.g., 'run', 'agents')
276
+
277
+ Returns:
278
+ Tuple of (mode, strategy) to use
279
+ """
280
+ strategy = DependencyStrategy()
281
+
282
+ # Override for specific commands
283
+ if command == 'agents' and 'deps-' in str(sys.argv):
284
+ # If running agents deps-* commands, don't check automatically
285
+ return (DependencyMode.OFF, strategy)
286
+
287
+ # Quick commands shouldn't check dependencies
288
+ quick_commands = ['help', 'version', 'info', 'tickets']
289
+ if command in quick_commands:
290
+ return (DependencyMode.OFF, strategy)
291
+
292
+ return (strategy.mode, strategy)
293
+
294
+
295
+ def lazy_check_agent_dependency(agent_id: str) -> bool:
296
+ """
297
+ Lazily check dependencies when a specific agent is invoked.
298
+
299
+ Args:
300
+ agent_id: The agent being invoked
301
+
302
+ Returns:
303
+ True if dependencies are satisfied or installed, False otherwise
304
+ """
305
+ from .agent_dependency_loader import AgentDependencyLoader
306
+
307
+ logger.debug(f"Lazy checking dependencies for agent: {agent_id}")
308
+
309
+ loader = AgentDependencyLoader(auto_install=False)
310
+ loader.discover_deployed_agents()
311
+
312
+ # Only check the specific agent
313
+ if agent_id not in loader.deployed_agents:
314
+ return True # Agent not deployed, no deps to check
315
+
316
+ loader.deployed_agents = {agent_id: loader.deployed_agents[agent_id]}
317
+ loader.load_agent_dependencies()
318
+ results = loader.analyze_dependencies()
319
+
320
+ agent_results = results['agents'].get(agent_id, {})
321
+ missing = agent_results.get('python', {}).get('missing', [])
322
+
323
+ if not missing:
324
+ return True
325
+
326
+ # Get strategy for handling missing deps
327
+ strategy = DependencyStrategy()
328
+
329
+ if strategy.mode == DependencyMode.AUTO:
330
+ logger.info(f"Auto-installing {len(missing)} dependencies for {agent_id}")
331
+ success, _ = loader.install_missing_dependencies(missing)
332
+ return success
333
+
334
+ elif strategy.mode == DependencyMode.INTERACTIVE:
335
+ choice = strategy.prompt_for_installation(missing)
336
+ if choice in ['yes']:
337
+ success, _ = loader.install_missing_dependencies(missing)
338
+ return success
339
+ return False
340
+
341
+ else: # CHECK or OFF
342
+ logger.warning(f"Agent {agent_id} missing {len(missing)} dependencies")
343
+ return False # Proceed anyway
@@ -0,0 +1,310 @@
1
+ """
2
+ Environment context detection for smart dependency checking.
3
+
4
+ This module determines the execution environment and whether interactive
5
+ prompting is appropriate for dependency installation.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ from typing import Dict, Tuple
11
+ import logging
12
+
13
+ from ..core.logger import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class EnvironmentContext:
19
+ """
20
+ Detects and analyzes the execution environment.
21
+
22
+ WHY: We need to know if we're in an environment where prompting makes sense.
23
+ Interactive prompting should only happen in TTY environments where a human
24
+ is present. CI/CD, Docker, and non-interactive contexts should skip prompts.
25
+
26
+ DESIGN DECISION: Use multiple indicators to detect environment type:
27
+ - TTY presence is the primary indicator for interactivity
28
+ - Environment variables help identify CI/CD and containerized environments
29
+ - Command-line flags can override automatic detection
30
+ """
31
+
32
+ # Known CI environment variables
33
+ CI_ENV_VARS = [
34
+ 'CI', 'CONTINUOUS_INTEGRATION', 'GITHUB_ACTIONS',
35
+ 'GITLAB_CI', 'JENKINS', 'TRAVIS', 'CIRCLECI',
36
+ 'BUILDKITE', 'DRONE', 'TEAMCITY_VERSION', 'TF_BUILD',
37
+ 'CODEBUILD_BUILD_ID', 'BITBUCKET_BUILD_NUMBER'
38
+ ]
39
+
40
+ # Docker/container indicators
41
+ CONTAINER_INDICATORS = [
42
+ '/.dockerenv', # Docker creates this file
43
+ '/run/.containerenv', # Podman creates this file
44
+ ]
45
+
46
+ @classmethod
47
+ def detect_execution_context(cls) -> Dict[str, bool]:
48
+ """
49
+ Detect the current execution context.
50
+
51
+ Returns:
52
+ Dictionary with context indicators:
53
+ - is_tty: True if running in a terminal with TTY
54
+ - is_ci: True if running in CI/CD environment
55
+ - is_docker: True if running inside Docker container
56
+ - is_interactive: True if interactive prompting is possible
57
+ - is_automated: True if running in automated context (CI or scheduled)
58
+ """
59
+ context = {
60
+ 'is_tty': cls._detect_tty(),
61
+ 'is_ci': cls._detect_ci(),
62
+ 'is_docker': cls._detect_docker(),
63
+ 'is_interactive': False,
64
+ 'is_automated': False,
65
+ 'is_jupyter': cls._detect_jupyter(),
66
+ 'is_ssh': cls._detect_ssh()
67
+ }
68
+
69
+ # Determine if we're in an automated context
70
+ context['is_automated'] = context['is_ci'] or cls._detect_automated()
71
+
72
+ # Interactive if TTY is available and not in automated context
73
+ context['is_interactive'] = (
74
+ context['is_tty'] and
75
+ not context['is_automated'] and
76
+ not context['is_docker'] # Docker usually non-interactive
77
+ )
78
+
79
+ logger.debug(f"Environment context detected: {context}")
80
+ return context
81
+
82
+ @classmethod
83
+ def _detect_tty(cls) -> bool:
84
+ """
85
+ Detect if we have a TTY (terminal) available.
86
+
87
+ WHY: TTY presence is the most reliable indicator that a human
88
+ is present and can respond to prompts.
89
+ """
90
+ try:
91
+ # Check if stdin is a terminal
92
+ has_stdin_tty = sys.stdin.isatty() if hasattr(sys.stdin, 'isatty') else False
93
+
94
+ # Check if stdout is a terminal (for output)
95
+ has_stdout_tty = sys.stdout.isatty() if hasattr(sys.stdout, 'isatty') else False
96
+
97
+ # Both should be TTY for interactive use
98
+ return has_stdin_tty and has_stdout_tty
99
+ except Exception as e:
100
+ logger.debug(f"TTY detection failed: {e}")
101
+ return False
102
+
103
+ @classmethod
104
+ def _detect_ci(cls) -> bool:
105
+ """
106
+ Detect if running in a CI/CD environment.
107
+
108
+ WHY: CI environments should never prompt for user input.
109
+ They need to run fully automated without human intervention.
110
+ """
111
+ # Check for common CI environment variables
112
+ for var in cls.CI_ENV_VARS:
113
+ if os.environ.get(var):
114
+ logger.debug(f"CI environment detected via {var}={os.environ.get(var)}")
115
+ return True
116
+
117
+ # Additional heuristics for CI detection
118
+ if os.environ.get('BUILD_ID') or os.environ.get('BUILD_NUMBER'):
119
+ return True
120
+
121
+ return False
122
+
123
+ @classmethod
124
+ def _detect_docker(cls) -> bool:
125
+ """
126
+ Detect if running inside a Docker container.
127
+
128
+ WHY: Docker containers typically run non-interactively,
129
+ and prompting for input often doesn't make sense.
130
+ """
131
+ # Check for Docker-specific files
132
+ for indicator_file in cls.CONTAINER_INDICATORS:
133
+ if os.path.exists(indicator_file):
134
+ logger.debug(f"Container detected via {indicator_file}")
135
+ return True
136
+
137
+ # Check for container-related environment variables
138
+ if os.environ.get('KUBERNETES_SERVICE_HOST'):
139
+ return True
140
+
141
+ # Check cgroup for docker/containerd references
142
+ try:
143
+ with open('/proc/1/cgroup', 'r') as f:
144
+ cgroup_content = f.read()
145
+ if 'docker' in cgroup_content or 'containerd' in cgroup_content:
146
+ return True
147
+ except (FileNotFoundError, PermissionError):
148
+ pass
149
+
150
+ return False
151
+
152
+ @classmethod
153
+ def _detect_automated(cls) -> bool:
154
+ """
155
+ Detect if running in an automated context (cron, systemd, etc).
156
+
157
+ WHY: Automated scripts should not prompt for input even if
158
+ they technically have TTY access.
159
+ """
160
+ # Check for cron execution
161
+ if os.environ.get('CRON') or not os.environ.get('TERM'):
162
+ return True
163
+
164
+ # Check for systemd service
165
+ if os.environ.get('INVOCATION_ID'): # systemd sets this
166
+ return True
167
+
168
+ return False
169
+
170
+ @classmethod
171
+ def _detect_jupyter(cls) -> bool:
172
+ """
173
+ Detect if running in Jupyter notebook/lab.
174
+
175
+ WHY: Jupyter has its own interaction model and standard
176
+ terminal prompts don't work well.
177
+ """
178
+ try:
179
+ # Check for IPython/Jupyter
180
+ get_ipython = globals().get('get_ipython')
181
+ if get_ipython is not None:
182
+ return True
183
+ except:
184
+ pass
185
+
186
+ # Check for Jupyter-specific environment variables
187
+ if 'JPY_PARENT_PID' in os.environ:
188
+ return True
189
+
190
+ return False
191
+
192
+ @classmethod
193
+ def _detect_ssh(cls) -> bool:
194
+ """
195
+ Detect if running over SSH.
196
+
197
+ WHY: SSH sessions might have TTY but prompting behavior
198
+ should be more conservative.
199
+ """
200
+ return 'SSH_CLIENT' in os.environ or 'SSH_TTY' in os.environ
201
+
202
+ @classmethod
203
+ def should_prompt_for_dependencies(
204
+ cls,
205
+ force_prompt: bool = False,
206
+ force_skip: bool = False
207
+ ) -> Tuple[bool, str]:
208
+ """
209
+ Determine if we should prompt for dependency installation.
210
+
211
+ Args:
212
+ force_prompt: Force prompting regardless of environment
213
+ force_skip: Force skipping prompts regardless of environment
214
+
215
+ Returns:
216
+ Tuple of (should_prompt, reason_message)
217
+
218
+ WHY: This is the main decision point for the smart dependency system.
219
+ We want to prompt only when it makes sense and is safe to do so.
220
+ """
221
+ # Handle forced flags
222
+ if force_skip:
223
+ return False, "Prompting disabled by --no-prompt flag"
224
+ if force_prompt:
225
+ return True, "Prompting forced by --prompt flag"
226
+
227
+ # Get environment context
228
+ context = cls.detect_execution_context()
229
+
230
+ # Decision logic with clear reasoning
231
+ if not context['is_tty']:
232
+ return False, "No TTY available for interactive prompts"
233
+
234
+ if context['is_ci']:
235
+ return False, "Running in CI environment - prompts disabled"
236
+
237
+ if context['is_docker']:
238
+ return False, "Running in Docker container - prompts disabled"
239
+
240
+ if context['is_automated']:
241
+ return False, "Running in automated context - prompts disabled"
242
+
243
+ if context['is_jupyter']:
244
+ return False, "Running in Jupyter - standard prompts not supported"
245
+
246
+ if context['is_interactive']:
247
+ return True, "Interactive TTY environment detected"
248
+
249
+ # Default to not prompting if uncertain
250
+ return False, "Environment type uncertain - prompts disabled for safety"
251
+
252
+ @classmethod
253
+ def get_environment_summary(cls) -> str:
254
+ """
255
+ Get a human-readable summary of the environment.
256
+
257
+ Returns:
258
+ String describing the detected environment.
259
+ """
260
+ context = cls.detect_execution_context()
261
+
262
+ env_types = []
263
+ if context['is_ci']:
264
+ env_types.append("CI/CD")
265
+ if context['is_docker']:
266
+ env_types.append("Docker")
267
+ if context['is_jupyter']:
268
+ env_types.append("Jupyter")
269
+ if context['is_ssh']:
270
+ env_types.append("SSH")
271
+ if context['is_automated']:
272
+ env_types.append("Automated")
273
+
274
+ if not env_types:
275
+ if context['is_interactive']:
276
+ env_types.append("Interactive Terminal")
277
+ else:
278
+ env_types.append("Non-interactive")
279
+
280
+ return f"Environment: {', '.join(env_types)} (TTY: {context['is_tty']})"
281
+
282
+
283
+ def detect_execution_context() -> Dict[str, bool]:
284
+ """
285
+ Convenience function to detect execution context.
286
+
287
+ Returns:
288
+ Dictionary with context indicators.
289
+ """
290
+ return EnvironmentContext.detect_execution_context()
291
+
292
+
293
+ def should_prompt_for_dependencies(
294
+ force_prompt: bool = False,
295
+ force_skip: bool = False
296
+ ) -> Tuple[bool, str]:
297
+ """
298
+ Convenience function to determine if prompting is appropriate.
299
+
300
+ Args:
301
+ force_prompt: Force prompting regardless of environment
302
+ force_skip: Force skipping prompts regardless of environment
303
+
304
+ Returns:
305
+ Tuple of (should_prompt, reason_message)
306
+ """
307
+ return EnvironmentContext.should_prompt_for_dependencies(
308
+ force_prompt=force_prompt,
309
+ force_skip=force_skip
310
+ )