claude-mpm 5.0.9__py3-none-any.whl → 5.4.41__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/BASE_AGENT.md +164 -0
- claude_mpm/agents/{PM_INSTRUCTIONS_TEACH.md → CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md} +721 -41
- claude_mpm/agents/MEMORY.md +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +468 -468
- claude_mpm/agents/WORKFLOW.md +5 -254
- claude_mpm/agents/agent_loader.py +13 -44
- claude_mpm/agents/base_agent.json +1 -1
- claude_mpm/agents/frontmatter_validator.py +70 -2
- claude_mpm/agents/templates/circuit-breakers.md +431 -45
- claude_mpm/cli/__init__.py +0 -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 +18 -27
- claude_mpm/cli/commands/agents.py +175 -37
- claude_mpm/cli/commands/auto_configure.py +723 -236
- claude_mpm/cli/commands/config.py +88 -2
- claude_mpm/cli/commands/configure.py +1262 -157
- claude_mpm/cli/commands/configure_agent_display.py +25 -6
- claude_mpm/cli/commands/mpm_init/core.py +225 -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/postmortem.py +1 -1
- claude_mpm/cli/commands/profile.py +277 -0
- claude_mpm/cli/commands/skills.py +214 -189
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +21 -3
- claude_mpm/cli/interactive/agent_wizard.py +85 -10
- claude_mpm/cli/parsers/agents_parser.py +54 -9
- claude_mpm/cli/parsers/auto_configure_parser.py +13 -138
- claude_mpm/cli/parsers/base_parser.py +12 -0
- claude_mpm/cli/parsers/config_parser.py +153 -83
- claude_mpm/cli/parsers/profile_parser.py +148 -0
- claude_mpm/cli/parsers/skills_parser.py +3 -2
- claude_mpm/cli/startup.py +879 -149
- claude_mpm/commands/mpm-config.md +28 -0
- claude_mpm/commands/mpm-doctor.md +9 -22
- claude_mpm/commands/mpm-help.md +5 -287
- claude_mpm/commands/mpm-init.md +81 -507
- claude_mpm/commands/mpm-monitor.md +15 -402
- claude_mpm/commands/mpm-organize.md +120 -0
- claude_mpm/commands/mpm-postmortem.md +6 -108
- claude_mpm/commands/mpm-session-resume.md +12 -363
- claude_mpm/commands/mpm-status.md +5 -69
- claude_mpm/commands/mpm-ticket-view.md +52 -495
- claude_mpm/commands/mpm-version.md +5 -107
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/core/config.py +2 -4
- claude_mpm/core/framework/formatters/content_formatter.py +3 -13
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/framework/loaders/instruction_loader.py +52 -11
- claude_mpm/core/framework_loader.py +4 -2
- claude_mpm/core/logger.py +13 -0
- claude_mpm/core/optimized_startup.py +59 -0
- claude_mpm/core/output_style_manager.py +173 -43
- claude_mpm/core/shared/config_loader.py +1 -1
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/unified_agent_registry.py +134 -16
- claude_mpm/core/unified_config.py +22 -0
- claude_mpm/dashboard/static/svelte-build/_app/env.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/0.B_FtCwCQ.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/assets/2.Cl_eSA4x.css +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/BgChzWQ1.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CIXEwuWe.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/CWc5urbQ.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DMkZpdF2.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/DjhvlsAc.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/N4qtv3Hx.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/chunks/uj46x2Wr.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/app.DTL5mJO-.js +2 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/entry/start.DzuEhzqh.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/0.CAGBuiOw.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/1.DFLC8jdE.js +1 -0
- claude_mpm/dashboard/static/svelte-build/_app/immutable/nodes/2.DPvEihJJ.js +10 -0
- claude_mpm/dashboard/static/svelte-build/_app/version.json +1 -0
- claude_mpm/dashboard/static/svelte-build/favicon.svg +7 -0
- claude_mpm/dashboard/static/svelte-build/index.html +36 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
- 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 +155 -1
- claude_mpm/hooks/claude_hooks/installer.py +33 -10
- claude_mpm/hooks/claude_hooks/memory_integration.py +28 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +30 -6
- claude_mpm/hooks/memory_integration_hook.py +46 -1
- claude_mpm/init.py +63 -19
- claude_mpm/models/agent_definition.py +7 -0
- claude_mpm/models/git_repository.py +3 -3
- 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_builder.py +3 -3
- claude_mpm/services/agents/agent_recommendation_service.py +278 -0
- claude_mpm/services/agents/agent_review_service.py +280 -0
- claude_mpm/services/agents/cache_git_manager.py +6 -6
- claude_mpm/services/agents/deployment/agent_deployment.py +29 -7
- claude_mpm/services/agents/deployment/agent_discovery_service.py +4 -5
- claude_mpm/services/agents/deployment/agent_template_builder.py +5 -3
- claude_mpm/services/agents/deployment/agents_directory_resolver.py +2 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +320 -29
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +546 -68
- claude_mpm/services/agents/git_source_manager.py +36 -2
- claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
- claude_mpm/services/agents/recommender.py +5 -3
- claude_mpm/services/agents/single_tier_deployment_service.py +2 -2
- claude_mpm/services/agents/sources/git_source_sync_service.py +13 -6
- claude_mpm/services/agents/startup_sync.py +22 -2
- 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/diagnostics/checks/agent_check.py +2 -2
- claude_mpm/services/diagnostics/checks/agent_sources_check.py +1 -1
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/git/git_operations_service.py +101 -16
- claude_mpm/services/monitor/daemon.py +9 -2
- claude_mpm/services/monitor/daemon_manager.py +39 -3
- claude_mpm/services/monitor/management/lifecycle.py +8 -1
- claude_mpm/services/monitor/server.py +698 -22
- claude_mpm/services/pm_skills_deployer.py +676 -0
- claude_mpm/services/profile_manager.py +331 -0
- claude_mpm/services/project/project_organizer.py +4 -0
- 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 +130 -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/dashboard_server.py +1 -0
- claude_mpm/services/socketio/event_normalizer.py +51 -6
- claude_mpm/services/socketio/server/core.py +386 -108
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/skills/skill_manager.py +92 -3
- claude_mpm/utils/agent_dependency_loader.py +14 -2
- claude_mpm/utils/agent_filters.py +17 -44
- claude_mpm/utils/gitignore.py +3 -0
- claude_mpm/utils/migration.py +4 -4
- claude_mpm/utils/robust_installer.py +47 -3
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/METADATA +57 -87
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/RECORD +160 -211
- claude_mpm-5.4.41.dist-info/entry_points.txt +5 -0
- claude_mpm-5.4.41.dist-info/licenses/LICENSE +94 -0
- claude_mpm-5.4.41.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_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_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.0.9.dist-info/entry_points.txt +0 -10
- claude_mpm-5.0.9.dist-info/licenses/LICENSE +0 -21
- /claude_mpm/agents/{OUTPUT_STYLE.md → CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/WHEEL +0 -0
- {claude_mpm-5.0.9.dist-info → claude_mpm-5.4.41.dist-info}/top_level.txt +0 -0
|
@@ -18,6 +18,11 @@ from dataclasses import dataclass, field
|
|
|
18
18
|
from datetime import datetime, timezone
|
|
19
19
|
from typing import Any, Dict, List, Optional
|
|
20
20
|
|
|
21
|
+
# Privileged users who can push directly to main branch
|
|
22
|
+
# All other users must use feature branches and PRs
|
|
23
|
+
PRIVILEGED_GIT_USERS = ["bobmatnyc@users.noreply.github.com"]
|
|
24
|
+
PROTECTED_BRANCHES = ["main", "master"]
|
|
25
|
+
|
|
21
26
|
|
|
22
27
|
@dataclass
|
|
23
28
|
class GitBranchInfo:
|
|
@@ -101,6 +106,94 @@ class GitOperationsManager:
|
|
|
101
106
|
if not self._is_git_repository():
|
|
102
107
|
raise GitOperationError(f"Not a Git repository: {project_root}")
|
|
103
108
|
|
|
109
|
+
def _get_current_git_user(self) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Get the current Git user email.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Git user email configured in repository or globally
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
GitOperationError: If git user.email is not configured
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
result = self._run_git_command(["config", "user.email"])
|
|
121
|
+
email = result.stdout.strip()
|
|
122
|
+
if not email:
|
|
123
|
+
raise GitOperationError(
|
|
124
|
+
"Git user.email is not configured. "
|
|
125
|
+
"Please configure it with: git config user.email 'your@email.com'"
|
|
126
|
+
)
|
|
127
|
+
return email
|
|
128
|
+
except GitOperationError as e:
|
|
129
|
+
raise GitOperationError(
|
|
130
|
+
"Git user.email is not configured. "
|
|
131
|
+
"Please configure it with: git config user.email 'your@email.com'"
|
|
132
|
+
) from e
|
|
133
|
+
|
|
134
|
+
def _is_privileged_user(self) -> bool:
|
|
135
|
+
"""
|
|
136
|
+
Check if the current Git user is privileged to push to protected branches.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
True if user email is in PRIVILEGED_GIT_USERS, False otherwise
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
current_user = self._get_current_git_user()
|
|
143
|
+
return current_user in PRIVILEGED_GIT_USERS
|
|
144
|
+
except GitOperationError:
|
|
145
|
+
# If we can't determine user, assume not privileged
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
def _enforce_branch_protection(
|
|
149
|
+
self, target_branch: str, operation: str
|
|
150
|
+
) -> Optional[GitOperationResult]:
|
|
151
|
+
"""
|
|
152
|
+
Enforce branch protection rules for protected branches.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
target_branch: Branch being operated on
|
|
156
|
+
operation: Operation being performed (e.g., "push", "merge")
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
GitOperationResult with error if protection violated, None if allowed
|
|
160
|
+
"""
|
|
161
|
+
# Check if target branch is protected
|
|
162
|
+
if target_branch not in PROTECTED_BRANCHES:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
# Check if user is privileged
|
|
166
|
+
if self._is_privileged_user():
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
# Get current user for error message
|
|
170
|
+
try:
|
|
171
|
+
current_user = self._get_current_git_user()
|
|
172
|
+
except GitOperationError:
|
|
173
|
+
current_user = "unknown"
|
|
174
|
+
|
|
175
|
+
# Build helpful error message
|
|
176
|
+
error_message = (
|
|
177
|
+
f"Direct {operation} to '{target_branch}' branch is restricted.\n"
|
|
178
|
+
f"Only {', '.join(PRIVILEGED_GIT_USERS)} can {operation} directly to protected branches.\n"
|
|
179
|
+
f"Current user: {current_user}\n\n"
|
|
180
|
+
f"Please use the feature branch workflow:\n"
|
|
181
|
+
f" 1. git checkout -b feature/your-feature-name\n"
|
|
182
|
+
f" 2. Make your changes and commit\n"
|
|
183
|
+
f" 3. git push -u origin feature/your-feature-name\n"
|
|
184
|
+
f" 4. Create a Pull Request on GitHub for review"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return GitOperationResult(
|
|
188
|
+
success=False,
|
|
189
|
+
operation=f"{operation}_branch_protection",
|
|
190
|
+
message=f"Branch protection: {operation} to '{target_branch}' denied",
|
|
191
|
+
error=error_message,
|
|
192
|
+
branch_before=self.get_current_branch(),
|
|
193
|
+
branch_after=self.get_current_branch(),
|
|
194
|
+
execution_time=0.0,
|
|
195
|
+
)
|
|
196
|
+
|
|
104
197
|
def _is_git_repository(self) -> bool:
|
|
105
198
|
"""Check if the directory is a Git repository."""
|
|
106
199
|
return self.git_dir.exists() and self.git_dir.is_dir()
|
|
@@ -503,6 +596,11 @@ class GitOperationsManager:
|
|
|
503
596
|
start_time = datetime.now(timezone.utc)
|
|
504
597
|
current_branch = self.get_current_branch()
|
|
505
598
|
|
|
599
|
+
# Enforce branch protection for target branch
|
|
600
|
+
protection_result = self._enforce_branch_protection(target_branch, "merge")
|
|
601
|
+
if protection_result:
|
|
602
|
+
return protection_result
|
|
603
|
+
|
|
506
604
|
try:
|
|
507
605
|
# Switch to target branch
|
|
508
606
|
if current_branch != target_branch:
|
|
@@ -659,6 +757,11 @@ class GitOperationsManager:
|
|
|
659
757
|
if not branch_name:
|
|
660
758
|
branch_name = current_branch
|
|
661
759
|
|
|
760
|
+
# Enforce branch protection
|
|
761
|
+
protection_result = self._enforce_branch_protection(branch_name, "push")
|
|
762
|
+
if protection_result:
|
|
763
|
+
return protection_result
|
|
764
|
+
|
|
662
765
|
try:
|
|
663
766
|
# Build push command
|
|
664
767
|
push_args = ["push"]
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Dict, List, Optional
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
6
8
|
|
|
7
9
|
from claude_mpm.core.logging_utils import get_logger
|
|
8
10
|
|
|
@@ -56,12 +58,79 @@ class SkillManager:
|
|
|
56
58
|
if mapping_count > 0:
|
|
57
59
|
logger.info(f"Loaded skill mappings for {mapping_count} agents")
|
|
58
60
|
|
|
61
|
+
def _get_pm_skills(self, project_dir: Optional[Path] = None) -> List[Dict[str, Any]]:
|
|
62
|
+
"""Load PM skills from project's .claude-mpm/skills/pm/ directory.
|
|
63
|
+
|
|
64
|
+
PM skills are special required skills deployed per-project,
|
|
65
|
+
NOT fetched from the skills repository.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
project_dir: Project directory. Defaults to current working directory.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of PM skill dictionaries with metadata
|
|
72
|
+
"""
|
|
73
|
+
if project_dir is None:
|
|
74
|
+
project_dir = Path.cwd()
|
|
75
|
+
|
|
76
|
+
pm_skills_dir = project_dir / ".claude-mpm" / "skills" / "pm"
|
|
77
|
+
|
|
78
|
+
if not pm_skills_dir.exists():
|
|
79
|
+
logger.debug("PM skills directory not found")
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
skills = []
|
|
83
|
+
for skill_dir in pm_skills_dir.iterdir():
|
|
84
|
+
if skill_dir.is_dir():
|
|
85
|
+
skill_file = skill_dir / "SKILL.md"
|
|
86
|
+
if skill_file.exists():
|
|
87
|
+
skill = self._load_pm_skill(skill_file)
|
|
88
|
+
if skill:
|
|
89
|
+
skills.append(skill)
|
|
90
|
+
|
|
91
|
+
if skills:
|
|
92
|
+
logger.debug(f"Loaded {len(skills)} PM skills from {pm_skills_dir}")
|
|
93
|
+
|
|
94
|
+
return skills
|
|
95
|
+
|
|
96
|
+
def _load_pm_skill(self, skill_file: Path) -> Optional[Dict[str, Any]]:
|
|
97
|
+
"""Load a single PM skill from SKILL.md file.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
skill_file: Path to SKILL.md file
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dictionary with skill metadata and content, or None if failed
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
content = skill_file.read_text(encoding='utf-8')
|
|
107
|
+
|
|
108
|
+
# Parse YAML frontmatter
|
|
109
|
+
if content.startswith('---'):
|
|
110
|
+
parts = content.split('---', 2)
|
|
111
|
+
if len(parts) >= 3:
|
|
112
|
+
metadata = yaml.safe_load(parts[1])
|
|
113
|
+
body = parts[2].strip()
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
'name': metadata.get('name', skill_file.parent.name),
|
|
117
|
+
'version': metadata.get('version', '1.0.0'),
|
|
118
|
+
'description': metadata.get('description', ''),
|
|
119
|
+
'when_to_use': metadata.get('when_to_use', ''),
|
|
120
|
+
'content': body,
|
|
121
|
+
'is_pm_skill': True
|
|
122
|
+
}
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.warning(f"Failed to load PM skill {skill_file}: {e}")
|
|
125
|
+
|
|
126
|
+
return None
|
|
127
|
+
|
|
59
128
|
def get_agent_skills(self, agent_type: str) -> List[Skill]:
|
|
60
129
|
"""
|
|
61
|
-
Get all skills for an agent (bundled + discovered).
|
|
130
|
+
Get all skills for an agent (bundled + discovered + PM skills if PM agent).
|
|
62
131
|
|
|
63
132
|
Args:
|
|
64
|
-
agent_type: Agent type/ID (e.g., 'engineer', 'python_engineer')
|
|
133
|
+
agent_type: Agent type/ID (e.g., 'engineer', 'python_engineer', 'pm')
|
|
65
134
|
|
|
66
135
|
Returns:
|
|
67
136
|
List of Skill objects for this agent
|
|
@@ -86,6 +155,26 @@ class SkillManager:
|
|
|
86
155
|
if skill not in skills:
|
|
87
156
|
skills.append(skill)
|
|
88
157
|
|
|
158
|
+
# Add PM skills for PM agent only
|
|
159
|
+
if agent_type.lower() in ('pm', 'project-manager', 'project_manager'):
|
|
160
|
+
pm_skill_dicts = self._get_pm_skills()
|
|
161
|
+
for pm_skill_dict in pm_skill_dicts:
|
|
162
|
+
# Convert PM skill dict to Skill object
|
|
163
|
+
pm_skill = Skill(
|
|
164
|
+
name=pm_skill_dict['name'],
|
|
165
|
+
path=Path.cwd() / ".claude-mpm" / "skills" / "pm" / pm_skill_dict['name'],
|
|
166
|
+
content=pm_skill_dict['content'],
|
|
167
|
+
source='pm', # Special source type for PM skills
|
|
168
|
+
version=pm_skill_dict['version'],
|
|
169
|
+
skill_id=pm_skill_dict['name'],
|
|
170
|
+
description=pm_skill_dict['description'],
|
|
171
|
+
agent_types=['pm', 'project-manager', 'project_manager']
|
|
172
|
+
)
|
|
173
|
+
skills.append(pm_skill)
|
|
174
|
+
|
|
175
|
+
if pm_skill_dicts:
|
|
176
|
+
logger.debug(f"Added {len(pm_skill_dicts)} PM skills for PM agent")
|
|
177
|
+
|
|
89
178
|
return skills
|
|
90
179
|
|
|
91
180
|
def enhance_agent_prompt(
|
|
@@ -658,12 +658,24 @@ class AgentDependencyLoader:
|
|
|
658
658
|
"Robust installer not available, falling back to simple installation"
|
|
659
659
|
)
|
|
660
660
|
try:
|
|
661
|
-
cmd = [sys.executable, "-m", "pip", "install"]
|
|
662
|
-
|
|
663
661
|
# Check environment and add appropriate flags
|
|
664
662
|
import os
|
|
665
663
|
import sysconfig
|
|
666
664
|
|
|
665
|
+
# Check if in UV tool environment (no pip available)
|
|
666
|
+
uv_tool_dir = os.environ.get("UV_TOOL_DIR", "")
|
|
667
|
+
is_uv_tool = (
|
|
668
|
+
(uv_tool_dir and "claude-mpm" in uv_tool_dir)
|
|
669
|
+
or ".local/share/uv/tools/" in sys.executable
|
|
670
|
+
or "/uv/tools/" in sys.executable
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
if is_uv_tool:
|
|
674
|
+
cmd = ["uv", "pip", "install"]
|
|
675
|
+
logger.debug("Using 'uv pip install' for UV tool environment")
|
|
676
|
+
else:
|
|
677
|
+
cmd = [sys.executable, "-m", "pip", "install"]
|
|
678
|
+
|
|
667
679
|
# Check if in virtualenv
|
|
668
680
|
in_virtualenv = (
|
|
669
681
|
(hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix)
|
|
@@ -4,10 +4,14 @@ Agent filtering utilities for claude-mpm.
|
|
|
4
4
|
WHY: This module provides centralized filtering logic to remove non-deployable
|
|
5
5
|
agents (BASE_AGENT) and already-deployed agents from user-facing displays.
|
|
6
6
|
|
|
7
|
+
ARCHITECTURE:
|
|
8
|
+
- SOURCE: ~/.claude-mpm/cache/agents/ (git repository cache)
|
|
9
|
+
- DEPLOYMENT: .claude/agents/ (project-level deployment location)
|
|
10
|
+
|
|
7
11
|
DESIGN DECISIONS:
|
|
8
12
|
- BASE_AGENT is a build tool, not a deployable agent - filter everywhere
|
|
9
|
-
- Deployed agent detection
|
|
10
|
-
|
|
13
|
+
- Deployed agent detection checks .claude/agents/ for all deployed agents
|
|
14
|
+
- Supports both virtual (.mpm_deployment_state) and physical (.md files) detection
|
|
11
15
|
- Case-insensitive BASE_AGENT detection for robustness
|
|
12
16
|
- Pure functions for easy testing and reuse
|
|
13
17
|
|
|
@@ -102,10 +106,11 @@ def get_deployed_agent_ids(project_dir: Optional[Path] = None) -> Set[str]:
|
|
|
102
106
|
|
|
103
107
|
Design Rationale:
|
|
104
108
|
- Primary detection: Virtual deployment state (.mpm_deployment_state)
|
|
105
|
-
- Fallback detection: Physical .md files
|
|
109
|
+
- Fallback detection: Physical .md files in .claude/agents/
|
|
106
110
|
- Returns leaf names for consistent comparison with agent_id formats
|
|
107
111
|
- Combines both detection methods for complete coverage
|
|
108
112
|
- Graceful error handling for malformed or missing state files
|
|
113
|
+
- Only checks project-level deployment (simplified architecture)
|
|
109
114
|
|
|
110
115
|
Related:
|
|
111
116
|
- Fixes checkbox interface showing all agents as "○ [Available]" instead of "● [Installed]"
|
|
@@ -115,24 +120,17 @@ def get_deployed_agent_ids(project_dir: Optional[Path] = None) -> Set[str]:
|
|
|
115
120
|
deployed = set()
|
|
116
121
|
|
|
117
122
|
# Track if project_dir was explicitly provided
|
|
118
|
-
explicit_project_dir = project_dir is not None
|
|
119
123
|
|
|
120
124
|
if project_dir is None:
|
|
121
125
|
project_dir = Path.cwd()
|
|
122
126
|
|
|
123
127
|
# NEW: Check virtual deployment state (primary method)
|
|
124
128
|
# This is the current deployment model used by Claude Code
|
|
129
|
+
# Only checking project-level deployment in simplified architecture
|
|
125
130
|
deployment_state_paths = [
|
|
126
131
|
project_dir / ".claude" / "agents" / ".mpm_deployment_state",
|
|
127
132
|
]
|
|
128
133
|
|
|
129
|
-
# Only check user-level state if using default project directory
|
|
130
|
-
# This prevents test isolation issues when explicit project_dir is provided
|
|
131
|
-
if not explicit_project_dir:
|
|
132
|
-
deployment_state_paths.append(
|
|
133
|
-
Path.home() / ".claude" / "agents" / ".mpm_deployment_state"
|
|
134
|
-
)
|
|
135
|
-
|
|
136
134
|
for state_path in deployment_state_paths:
|
|
137
135
|
if state_path.exists():
|
|
138
136
|
try:
|
|
@@ -162,42 +160,17 @@ def get_deployed_agent_ids(project_dir: Optional[Path] = None) -> Set[str]:
|
|
|
162
160
|
continue
|
|
163
161
|
|
|
164
162
|
# EXISTING: Check physical .md files (fallback for backward compatibility)
|
|
165
|
-
# Check
|
|
166
|
-
|
|
167
|
-
if
|
|
168
|
-
for file in
|
|
169
|
-
if file.stem not in {"BASE-AGENT", ".DS_Store"}:
|
|
170
|
-
deployed.add(file.stem)
|
|
171
|
-
|
|
172
|
-
# Check legacy architecture
|
|
173
|
-
legacy_agents_dir = project_dir / ".claude" / "agents"
|
|
174
|
-
if legacy_agents_dir.exists():
|
|
175
|
-
for file in legacy_agents_dir.glob("*.md"):
|
|
163
|
+
# Check project deployment location (.claude/agents/)
|
|
164
|
+
agents_dir = project_dir / ".claude" / "agents"
|
|
165
|
+
if agents_dir.exists():
|
|
166
|
+
for file in agents_dir.glob("*.md"):
|
|
176
167
|
if file.stem not in {"BASE-AGENT", ".DS_Store"}:
|
|
177
168
|
deployed.add(file.stem)
|
|
178
169
|
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if file.stem not in {
|
|
184
|
-
"BASE-AGENT",
|
|
185
|
-
".DS_Store",
|
|
186
|
-
"README",
|
|
187
|
-
"circuit-breakers",
|
|
188
|
-
}:
|
|
189
|
-
# Skip template/example files
|
|
190
|
-
if not any(x in file.stem for x in ["example", "template", "pm-"]):
|
|
191
|
-
deployed.add(file.stem)
|
|
192
|
-
|
|
193
|
-
# Check user-level directory only if using default project directory
|
|
194
|
-
# This prevents test isolation issues when explicit project_dir is provided
|
|
195
|
-
if not explicit_project_dir:
|
|
196
|
-
user_agents_dir = Path.home() / ".claude" / "agents"
|
|
197
|
-
if user_agents_dir.exists():
|
|
198
|
-
for file in user_agents_dir.glob("*.md"):
|
|
199
|
-
if file.stem not in {"BASE-AGENT", ".DS_Store"}:
|
|
200
|
-
deployed.add(file.stem)
|
|
170
|
+
# NOTE: .claude/templates/ contains PM instruction templates, NOT deployed agents
|
|
171
|
+
# It should NOT be checked here. Agents are deployed to:
|
|
172
|
+
# - .mpm_deployment_state (virtual deployment)
|
|
173
|
+
# - .claude/agents/*.md (project deployment)
|
|
201
174
|
|
|
202
175
|
return deployed
|
|
203
176
|
|
claude_mpm/utils/gitignore.py
CHANGED
|
@@ -212,6 +212,9 @@ def ensure_claude_mpm_gitignore(project_dir: str = ".") -> dict:
|
|
|
212
212
|
entries_to_add = [
|
|
213
213
|
".claude-mpm/",
|
|
214
214
|
".claude/agents/",
|
|
215
|
+
".mcp.json",
|
|
216
|
+
".claude.json",
|
|
217
|
+
".claude/",
|
|
215
218
|
]
|
|
216
219
|
|
|
217
220
|
added, existing = manager.ensure_entries(entries_to_add)
|
claude_mpm/utils/migration.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
WHY: Phase 3 of 1M-486 requires migrating from old single-tier deployment
|
|
4
4
|
(~/.claude/agents/, ~/.claude/skills/) to new two-phase architecture:
|
|
5
|
-
- Cache: ~/.claude-mpm/cache/
|
|
5
|
+
- Cache: ~/.claude-mpm/cache/agents/, ~/.claude-mpm/cache/skills/
|
|
6
6
|
- Deployment: .claude-mpm/agents/, .claude-mpm/skills/
|
|
7
7
|
|
|
8
8
|
DESIGN DECISIONS:
|
|
@@ -41,7 +41,7 @@ class MigrationUtility:
|
|
|
41
41
|
self.old_agent_dir = Path.home() / ".claude" / "agents"
|
|
42
42
|
self.old_skill_dir = Path.home() / ".claude" / "skills"
|
|
43
43
|
|
|
44
|
-
self.new_agent_cache = Path.home() / ".claude-mpm" / "cache" / "
|
|
44
|
+
self.new_agent_cache = Path.home() / ".claude-mpm" / "cache" / "agents"
|
|
45
45
|
self.new_skill_cache = Path.home() / ".claude-mpm" / "cache" / "skills"
|
|
46
46
|
|
|
47
47
|
def detect_old_locations(self) -> Dict[str, bool]:
|
|
@@ -80,7 +80,7 @@ class MigrationUtility:
|
|
|
80
80
|
def migrate_agents(
|
|
81
81
|
self, dry_run: bool = False, auto_confirm: bool = False
|
|
82
82
|
) -> Dict[str, any]:
|
|
83
|
-
"""Migrate agents from ~/.claude/agents/ to ~/.claude-mpm/cache/
|
|
83
|
+
"""Migrate agents from ~/.claude/agents/ to ~/.claude-mpm/cache/agents/.
|
|
84
84
|
|
|
85
85
|
Args:
|
|
86
86
|
dry_run: Preview migration without making changes
|
|
@@ -329,7 +329,7 @@ class MigrationUtility:
|
|
|
329
329
|
|
|
330
330
|
warning += "\nThe deployment architecture has changed:\n"
|
|
331
331
|
warning += " OLD: ~/.claude/agents/ (single-tier, global)\n"
|
|
332
|
-
warning += " NEW: ~/.claude-mpm/cache/
|
|
332
|
+
warning += " NEW: ~/.claude-mpm/cache/agents/ → .claude-mpm/agents/ (two-phase, per-project)\n\n"
|
|
333
333
|
|
|
334
334
|
warning += "To migrate:\n"
|
|
335
335
|
warning += " claude-mpm migrate\n\n"
|
|
@@ -82,6 +82,7 @@ class RobustPackageInstaller:
|
|
|
82
82
|
self.attempts: List[InstallAttempt] = []
|
|
83
83
|
self.success_cache: Dict[str, bool] = {}
|
|
84
84
|
self.in_virtualenv = self._check_virtualenv()
|
|
85
|
+
self.is_uv_tool = self._check_uv_tool_installation()
|
|
85
86
|
self.is_pep668_managed = self._check_pep668_managed()
|
|
86
87
|
self.pep668_warning_shown = False
|
|
87
88
|
|
|
@@ -261,6 +262,36 @@ class RobustPackageInstaller:
|
|
|
261
262
|
|
|
262
263
|
return False
|
|
263
264
|
|
|
265
|
+
def _check_uv_tool_installation(self) -> bool:
|
|
266
|
+
"""
|
|
267
|
+
Check if running in UV tool environment (no pip available).
|
|
268
|
+
|
|
269
|
+
WHY: UV tool environments don't have pip installed. The executable
|
|
270
|
+
path typically contains ".local/share/uv/tools/" and the UV_TOOL_DIR
|
|
271
|
+
environment variable is set. In such environments, we need to use
|
|
272
|
+
'uv pip' instead of 'python -m pip'.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
True if UV tool environment, False otherwise
|
|
276
|
+
"""
|
|
277
|
+
import os
|
|
278
|
+
|
|
279
|
+
# Check UV_TOOL_DIR environment variable
|
|
280
|
+
uv_tool_dir = os.environ.get("UV_TOOL_DIR", "")
|
|
281
|
+
if uv_tool_dir and "claude-mpm" in uv_tool_dir:
|
|
282
|
+
logger.debug(f"UV tool environment detected via UV_TOOL_DIR: {uv_tool_dir}")
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
# Check executable path for UV tool patterns
|
|
286
|
+
executable = sys.executable
|
|
287
|
+
if ".local/share/uv/tools/" in executable or "/uv/tools/" in executable:
|
|
288
|
+
logger.debug(
|
|
289
|
+
f"UV tool environment detected via executable path: {executable}"
|
|
290
|
+
)
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
return False
|
|
294
|
+
|
|
264
295
|
def _show_pep668_warning(self) -> None:
|
|
265
296
|
"""
|
|
266
297
|
Show warning about PEP 668 managed environment.
|
|
@@ -301,7 +332,12 @@ class RobustPackageInstaller:
|
|
|
301
332
|
Returns:
|
|
302
333
|
Command as list of arguments
|
|
303
334
|
"""
|
|
304
|
-
|
|
335
|
+
# UV tool environments don't have pip; use uv pip instead
|
|
336
|
+
if self.is_uv_tool:
|
|
337
|
+
base_cmd = ["uv", "pip", "install"]
|
|
338
|
+
logger.debug("Using 'uv pip install' for UV tool environment")
|
|
339
|
+
else:
|
|
340
|
+
base_cmd = [sys.executable, "-m", "pip", "install"]
|
|
305
341
|
|
|
306
342
|
# Determine appropriate flags based on environment
|
|
307
343
|
if self.in_virtualenv:
|
|
@@ -651,7 +687,12 @@ class RobustPackageInstaller:
|
|
|
651
687
|
Tuple of (success, error_message)
|
|
652
688
|
"""
|
|
653
689
|
try:
|
|
654
|
-
|
|
690
|
+
# UV tool environments don't have pip; use uv pip instead
|
|
691
|
+
if self.is_uv_tool:
|
|
692
|
+
cmd = ["uv", "pip", "install"]
|
|
693
|
+
logger.debug("Using 'uv pip install' for batch installation")
|
|
694
|
+
else:
|
|
695
|
+
cmd = [sys.executable, "-m", "pip", "install"]
|
|
655
696
|
|
|
656
697
|
# Add appropriate flags based on environment
|
|
657
698
|
if self.in_virtualenv:
|
|
@@ -702,7 +743,10 @@ class RobustPackageInstaller:
|
|
|
702
743
|
|
|
703
744
|
# Add environment status
|
|
704
745
|
lines.append("")
|
|
705
|
-
if self.
|
|
746
|
+
if self.is_uv_tool:
|
|
747
|
+
lines.append("✓ Environment: UV Tool Environment")
|
|
748
|
+
lines.append(" Using 'uv pip' command (pip not available)")
|
|
749
|
+
elif self.in_virtualenv:
|
|
706
750
|
lines.append("✓ Environment: Virtual Environment (isolated)")
|
|
707
751
|
lines.append(" No special pip flags needed")
|
|
708
752
|
elif self.is_pep668_managed:
|