claude-mpm 4.0.22__py3-none-any.whl → 4.0.25__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 (70) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +4 -1
  4. claude_mpm/agents/BASE_PM.md +3 -0
  5. claude_mpm/agents/templates/code_analyzer.json +3 -3
  6. claude_mpm/agents/templates/data_engineer.json +2 -2
  7. claude_mpm/agents/templates/documentation.json +36 -9
  8. claude_mpm/agents/templates/engineer.json +2 -2
  9. claude_mpm/agents/templates/ops.json +2 -2
  10. claude_mpm/agents/templates/qa.json +2 -2
  11. claude_mpm/agents/templates/refactoring_engineer.json +65 -43
  12. claude_mpm/agents/templates/security.json +2 -2
  13. claude_mpm/agents/templates/version_control.json +2 -2
  14. claude_mpm/agents/templates/web_ui.json +2 -2
  15. claude_mpm/cli/commands/agents.py +453 -113
  16. claude_mpm/cli/commands/aggregate.py +107 -15
  17. claude_mpm/cli/commands/cleanup.py +142 -10
  18. claude_mpm/cli/commands/config.py +358 -224
  19. claude_mpm/cli/commands/info.py +184 -75
  20. claude_mpm/cli/commands/mcp_command_router.py +5 -76
  21. claude_mpm/cli/commands/mcp_install_commands.py +68 -36
  22. claude_mpm/cli/commands/mcp_server_commands.py +30 -37
  23. claude_mpm/cli/commands/memory.py +331 -61
  24. claude_mpm/cli/commands/monitor.py +101 -7
  25. claude_mpm/cli/commands/run.py +368 -8
  26. claude_mpm/cli/commands/tickets.py +206 -24
  27. claude_mpm/cli/parsers/mcp_parser.py +3 -0
  28. claude_mpm/cli/shared/__init__.py +40 -0
  29. claude_mpm/cli/shared/argument_patterns.py +212 -0
  30. claude_mpm/cli/shared/command_base.py +234 -0
  31. claude_mpm/cli/shared/error_handling.py +238 -0
  32. claude_mpm/cli/shared/output_formatters.py +231 -0
  33. claude_mpm/config/agent_config.py +29 -8
  34. claude_mpm/core/container.py +6 -4
  35. claude_mpm/core/framework_loader.py +32 -9
  36. claude_mpm/core/service_registry.py +4 -2
  37. claude_mpm/core/shared/__init__.py +17 -0
  38. claude_mpm/core/shared/config_loader.py +320 -0
  39. claude_mpm/core/shared/path_resolver.py +277 -0
  40. claude_mpm/core/shared/singleton_manager.py +208 -0
  41. claude_mpm/hooks/claude_hooks/memory_integration.py +4 -2
  42. claude_mpm/hooks/claude_hooks/response_tracking.py +14 -3
  43. claude_mpm/hooks/memory_integration_hook.py +11 -2
  44. claude_mpm/services/agents/deployment/agent_deployment.py +43 -23
  45. claude_mpm/services/agents/deployment/deployment_wrapper.py +71 -0
  46. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +1 -0
  47. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +43 -0
  48. claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +4 -0
  49. claude_mpm/services/agents/deployment/processors/agent_processor.py +1 -1
  50. claude_mpm/services/agents/loading/base_agent_manager.py +11 -3
  51. claude_mpm/services/agents/registry/deployed_agent_discovery.py +14 -5
  52. claude_mpm/services/event_aggregator.py +4 -2
  53. claude_mpm/services/mcp_gateway/config/config_loader.py +89 -28
  54. claude_mpm/services/mcp_gateway/config/configuration.py +29 -0
  55. claude_mpm/services/mcp_gateway/registry/service_registry.py +22 -5
  56. claude_mpm/services/memory/builder.py +6 -1
  57. claude_mpm/services/response_tracker.py +3 -1
  58. claude_mpm/services/runner_configuration_service.py +15 -6
  59. claude_mpm/services/shared/__init__.py +20 -0
  60. claude_mpm/services/shared/async_service_base.py +219 -0
  61. claude_mpm/services/shared/config_service_base.py +292 -0
  62. claude_mpm/services/shared/lifecycle_service_base.py +317 -0
  63. claude_mpm/services/shared/manager_base.py +303 -0
  64. claude_mpm/services/shared/service_factory.py +308 -0
  65. {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/METADATA +19 -13
  66. {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/RECORD +70 -54
  67. {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/WHEEL +0 -0
  68. {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/entry_points.txt +0 -0
  69. {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/licenses/LICENSE +0 -0
  70. {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/top_level.txt +0 -0
@@ -10,6 +10,8 @@ This module handles:
10
10
  - Environment variable support for agent paths
11
11
  - Project-specific agent overrides
12
12
  - Tier precedence configuration
13
+
14
+ UPDATED: Migrated to use shared ConfigLoader pattern (TSK-0141)
13
15
  """
14
16
 
15
17
  import json
@@ -21,6 +23,7 @@ from pathlib import Path
21
23
  from typing import Any, Dict, List, Optional
22
24
 
23
25
  from claude_mpm.core.unified_paths import get_path_manager
26
+ from claude_mpm.core.shared.config_loader import ConfigLoader, ConfigPattern
24
27
 
25
28
  logger = logging.getLogger(__name__)
26
29
 
@@ -62,6 +65,9 @@ class AgentConfig:
62
65
  validate_on_load: bool = True
63
66
  strict_validation: bool = False
64
67
 
68
+ # ConfigLoader instance for consistent configuration loading
69
+ _config_loader: ConfigLoader = field(default_factory=ConfigLoader, init=False)
70
+
65
71
  @classmethod
66
72
  def from_environment(cls) -> "AgentConfig":
67
73
  """
@@ -147,14 +153,29 @@ class AgentConfig:
147
153
  return cls()
148
154
 
149
155
  try:
150
- if config_file.suffix in [".yaml", ".yml"]:
151
- import yaml
152
-
153
- with open(config_file, "r") as f:
154
- data = yaml.safe_load(f)
155
- else:
156
- with open(config_file, "r") as f:
157
- data = json.load(f)
156
+ # Use ConfigLoader for consistent file loading
157
+ config_loader = ConfigLoader()
158
+
159
+ # Create a pattern for agent configuration
160
+ pattern = ConfigPattern(
161
+ filenames=[config_file.name],
162
+ search_paths=[str(config_file.parent)],
163
+ env_prefix="CLAUDE_MPM_AGENT_",
164
+ defaults={
165
+ "enable_project_agents": True,
166
+ "enable_user_agents": True,
167
+ "enable_system_agents": True,
168
+ "enable_hot_reload": True,
169
+ "cache_ttl_seconds": 3600,
170
+ "enable_caching": True,
171
+ "validate_on_load": True,
172
+ "strict_validation": False,
173
+ "precedence_mode": "override"
174
+ }
175
+ )
176
+
177
+ loaded_config = config_loader.load_config(pattern, cache_key=f"agent_{config_file}")
178
+ data = loaded_config.to_dict()
158
179
 
159
180
  config = cls()
160
181
 
@@ -250,8 +250,9 @@ class DIContainer(IServiceContainer):
250
250
  instance: Pre-created instance to register as singleton
251
251
 
252
252
  Examples:
253
- # Register a pre-created instance
254
- config = Config()
253
+ # Register a pre-created instance using ConfigLoader
254
+ config_loader = ConfigLoader()
255
+ config = config_loader.load_main_config()
255
256
  container.register_instance(IConfig, config)
256
257
  """
257
258
  self._register_internal(
@@ -307,8 +308,9 @@ class DIContainer(IServiceContainer):
307
308
  # Register with implementation class
308
309
  container.register_singleton(ILogger, ConsoleLogger)
309
310
 
310
- # Register with instance
311
- container.register_singleton(IConfig, instance=Config())
311
+ # Register with instance using ConfigLoader
312
+ config_loader = ConfigLoader()
313
+ container.register_singleton(IConfig, instance=config_loader.load_main_config())
312
314
 
313
315
  # Register with disposal handler
314
316
  container.register_singleton(
@@ -640,9 +640,10 @@ class FrameworkLoader:
640
640
  Aggregate multiple memory entries into a single memory string.
641
641
 
642
642
  Strategy:
643
- - Parse memories by sections
644
- - Merge sections, with project-level taking precedence
645
- - Remove exact duplicates within sections
643
+ - Support both sectioned and non-sectioned memories
644
+ - Preserve all bullet-point items (lines starting with -)
645
+ - Merge sections when present, with project-level taking precedence
646
+ - Remove exact duplicates within sections and unsectioned items
646
647
  - Preserve unique entries from both sources
647
648
 
648
649
  Args:
@@ -658,15 +659,16 @@ class FrameworkLoader:
658
659
  if len(memory_entries) == 1:
659
660
  return memory_entries[0]["content"]
660
661
 
661
- # Parse all memories into sections
662
+ # Parse all memories into sections and unsectioned items
662
663
  all_sections = {}
664
+ unsectioned_items = {} # Items without a section header
663
665
  metadata_lines = []
664
666
 
665
667
  for entry in memory_entries:
666
668
  content = entry["content"]
667
669
  source = entry["source"]
668
670
 
669
- # Parse content into sections
671
+ # Parse content into sections and unsectioned items
670
672
  current_section = None
671
673
  current_items = []
672
674
 
@@ -690,11 +692,25 @@ class FrameworkLoader:
690
692
  # Start new section
691
693
  current_section = line
692
694
  current_items = []
693
- # Check for content lines (skip empty lines)
694
- elif line.strip() and current_section:
695
- current_items.append(line)
695
+ # Check for content lines (including unsectioned bullet points)
696
+ elif line.strip():
697
+ # If it's a bullet point or regular content
698
+ if current_section:
699
+ # Add to current section
700
+ current_items.append(line)
701
+ elif line.strip().startswith('-'):
702
+ # It's an unsectioned bullet point - preserve it
703
+ # Use content as key to detect duplicates
704
+ # Project source overrides user source
705
+ if line not in unsectioned_items or source == "project":
706
+ unsectioned_items[line] = source
707
+ # Skip other non-bullet unsectioned content (like headers)
708
+ elif not line.strip().startswith('#'):
709
+ # Include non-header orphaned content in unsectioned items
710
+ if line not in unsectioned_items or source == "project":
711
+ unsectioned_items[line] = source
696
712
 
697
- # Save last section
713
+ # Save last section if exists
698
714
  if current_section and current_items:
699
715
  if current_section not in all_sections:
700
716
  all_sections[current_section] = {}
@@ -717,6 +733,13 @@ class FrameworkLoader:
717
733
  lines.append("*This memory combines user-level and project-level memories.*")
718
734
  lines.append("")
719
735
 
736
+ # Add unsectioned items first (if any)
737
+ if unsectioned_items:
738
+ # Sort items to ensure consistent output
739
+ for item in sorted(unsectioned_items.keys()):
740
+ lines.append(item)
741
+ lines.append("") # Empty line after unsectioned items
742
+
720
743
  # Add sections
721
744
  for section_header in sorted(all_sections.keys()):
722
745
  lines.append(section_header)
@@ -11,6 +11,7 @@ from .base_service import BaseService
11
11
  from .config import Config
12
12
  from .container import DIContainer, ServiceLifetime, get_container
13
13
  from .logger import get_logger
14
+ from .shared.config_loader import ConfigLoader
14
15
 
15
16
  if TYPE_CHECKING:
16
17
  from claude_mpm.services.agents.deployment import AgentDeploymentService
@@ -46,8 +47,9 @@ class ServiceRegistry:
46
47
  from .agent_session_manager import AgentSessionManager
47
48
  from .session_manager import SessionManager
48
49
 
49
- # Register configuration as singleton with name
50
- config = Config()
50
+ # Register configuration as singleton with name using ConfigLoader
51
+ config_loader = ConfigLoader()
52
+ config = config_loader.load_main_config()
51
53
  self.container.register_singleton(Config, instance=config, name="main_config")
52
54
 
53
55
  # Register core services with proper lifetime management
@@ -0,0 +1,17 @@
1
+ """
2
+ Shared core utilities to reduce code duplication.
3
+
4
+ This module provides common utilities that can be used across
5
+ different parts of the application.
6
+ """
7
+
8
+ from .config_loader import ConfigLoader, ConfigPattern
9
+ from .path_resolver import PathResolver
10
+ from .singleton_manager import SingletonManager
11
+
12
+ __all__ = [
13
+ "ConfigLoader",
14
+ "ConfigPattern",
15
+ "PathResolver",
16
+ "SingletonManager",
17
+ ]
@@ -0,0 +1,320 @@
1
+ """
2
+ Shared configuration loading utilities to reduce duplication.
3
+ """
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ from ..config import Config
11
+ from ..logger import get_logger
12
+
13
+
14
+ @dataclass
15
+ class ConfigPattern:
16
+ """Configuration loading pattern definition."""
17
+
18
+ # File patterns to search for
19
+ filenames: List[str]
20
+
21
+ # Search paths (relative to working directory)
22
+ search_paths: List[str]
23
+
24
+ # Environment variable prefix
25
+ env_prefix: Optional[str] = None
26
+
27
+ # Default values
28
+ defaults: Optional[Dict[str, Any]] = None
29
+
30
+ # Required configuration keys
31
+ required_keys: Optional[List[str]] = None
32
+
33
+ # Configuration section name
34
+ section: Optional[str] = None
35
+
36
+
37
+ class ConfigLoader:
38
+ """
39
+ Centralized configuration loading utility.
40
+
41
+ Reduces duplication by providing standard patterns for:
42
+ - Configuration file discovery
43
+ - Environment variable loading
44
+ - Default value management
45
+ - Configuration validation
46
+ """
47
+
48
+ # Standard configuration patterns
49
+ AGENT_CONFIG = ConfigPattern(
50
+ filenames=[".agent.yaml", ".agent.yml", "agent.yaml", "agent.yml"],
51
+ search_paths=[".", ".claude-mpm", "agents"],
52
+ env_prefix="CLAUDE_MPM_AGENT_",
53
+ defaults={
54
+ "timeout": 30,
55
+ "max_retries": 3,
56
+ "log_level": "INFO"
57
+ },
58
+ required_keys=["name", "model"]
59
+ )
60
+
61
+ MEMORY_CONFIG = ConfigPattern(
62
+ filenames=[".memory.yaml", ".memory.yml", "memory.yaml", "memory.yml"],
63
+ search_paths=[".", ".claude-mpm", "memories"],
64
+ env_prefix="CLAUDE_MPM_MEMORY_",
65
+ defaults={
66
+ "max_entries": 1000,
67
+ "cleanup_interval": 3600,
68
+ "compression": True
69
+ }
70
+ )
71
+
72
+ SERVICE_CONFIG = ConfigPattern(
73
+ filenames=[".service.yaml", ".service.yml", "service.yaml", "service.yml"],
74
+ search_paths=[".", ".claude-mpm", "config"],
75
+ env_prefix="CLAUDE_MPM_SERVICE_",
76
+ defaults={
77
+ "enabled": True,
78
+ "auto_start": False,
79
+ "health_check_interval": 60
80
+ }
81
+ )
82
+
83
+ def __init__(self, working_dir: Union[str, Path] = None):
84
+ """
85
+ Initialize config loader.
86
+
87
+ Args:
88
+ working_dir: Working directory for relative paths
89
+ """
90
+ self.working_dir = Path(working_dir) if working_dir else Path.cwd()
91
+ self.logger = get_logger("config_loader")
92
+ self._cache: Dict[str, Config] = {}
93
+
94
+ def load_config(self,
95
+ pattern: ConfigPattern,
96
+ cache_key: str = None,
97
+ force_reload: bool = False) -> Config:
98
+ """
99
+ Load configuration using a pattern.
100
+
101
+ Args:
102
+ pattern: Configuration pattern to use
103
+ cache_key: Optional cache key (defaults to pattern hash)
104
+ force_reload: Force reload even if cached
105
+
106
+ Returns:
107
+ Loaded configuration
108
+ """
109
+ # Generate cache key if not provided
110
+ if cache_key is None:
111
+ cache_key = self._generate_cache_key(pattern)
112
+
113
+ # Check cache
114
+ if not force_reload and cache_key in self._cache:
115
+ self.logger.debug(f"Using cached config: {cache_key}")
116
+ return self._cache[cache_key]
117
+
118
+ # Start with defaults
119
+ config_data = pattern.defaults.copy() if pattern.defaults else {}
120
+
121
+ # Load from files
122
+ config_file = self._find_config_file(pattern)
123
+ if config_file:
124
+ file_config = self._load_config_file(config_file)
125
+ if pattern.section:
126
+ file_config = file_config.get(pattern.section, {})
127
+ config_data.update(file_config)
128
+ self.logger.info(f"Loaded config from: {config_file}")
129
+
130
+ # Load from environment
131
+ if pattern.env_prefix:
132
+ env_config = self._load_env_config(pattern.env_prefix)
133
+ config_data.update(env_config)
134
+ if env_config:
135
+ self.logger.debug(f"Loaded {len(env_config)} env vars with prefix {pattern.env_prefix}")
136
+
137
+ # Create config instance
138
+ config = Config(config_data)
139
+
140
+ # Validate required keys
141
+ if pattern.required_keys:
142
+ self._validate_required_keys(config, pattern.required_keys)
143
+
144
+ # Cache the result
145
+ self._cache[cache_key] = config
146
+
147
+ return config
148
+
149
+ def load_agent_config(self, agent_dir: Union[str, Path] = None) -> Config:
150
+ """Load agent configuration."""
151
+ pattern = self.AGENT_CONFIG
152
+ if agent_dir:
153
+ # Override search paths for specific agent directory
154
+ pattern = ConfigPattern(
155
+ filenames=pattern.filenames,
156
+ search_paths=[str(agent_dir)],
157
+ env_prefix=pattern.env_prefix,
158
+ defaults=pattern.defaults,
159
+ required_keys=pattern.required_keys
160
+ )
161
+
162
+ return self.load_config(pattern, cache_key=f"agent_{agent_dir}")
163
+
164
+ def load_main_config(self) -> Config:
165
+ """Load main application configuration."""
166
+ pattern = ConfigPattern(
167
+ filenames=["claude-mpm.yaml", "claude-mpm.yml", ".claude-mpm.yaml", ".claude-mpm.yml", "config.yaml", "config.yml"],
168
+ search_paths=["~/.config/claude-mpm", ".", "./config", "/etc/claude-mpm"],
169
+ env_prefix="CLAUDE_MPM_",
170
+ defaults={}
171
+ )
172
+ return self.load_config(pattern, cache_key="main_config")
173
+
174
+ def load_memory_config(self, memory_dir: Union[str, Path] = None) -> Config:
175
+ """Load memory configuration."""
176
+ pattern = self.MEMORY_CONFIG
177
+ if memory_dir:
178
+ pattern = ConfigPattern(
179
+ filenames=pattern.filenames,
180
+ search_paths=[str(memory_dir)],
181
+ env_prefix=pattern.env_prefix,
182
+ defaults=pattern.defaults
183
+ )
184
+
185
+ return self.load_config(pattern, cache_key=f"memory_{memory_dir}")
186
+
187
+ def load_service_config(self, service_name: str, config_dir: Union[str, Path] = None) -> Config:
188
+ """Load service configuration."""
189
+ pattern = self.SERVICE_CONFIG
190
+
191
+ # Add service-specific filenames
192
+ service_filenames = [
193
+ f".{service_name}.yaml",
194
+ f".{service_name}.yml",
195
+ f"{service_name}.yaml",
196
+ f"{service_name}.yml"
197
+ ]
198
+
199
+ pattern = ConfigPattern(
200
+ filenames=service_filenames + pattern.filenames,
201
+ search_paths=[str(config_dir)] if config_dir else pattern.search_paths,
202
+ env_prefix=f"CLAUDE_MPM_{service_name.upper()}_",
203
+ defaults=pattern.defaults,
204
+ section=service_name
205
+ )
206
+
207
+ return self.load_config(pattern, cache_key=f"service_{service_name}")
208
+
209
+ def _find_config_file(self, pattern: ConfigPattern) -> Optional[Path]:
210
+ """Find configuration file using pattern."""
211
+ for search_path in pattern.search_paths:
212
+ search_dir = self.working_dir / search_path
213
+ if not search_dir.exists():
214
+ continue
215
+
216
+ for filename in pattern.filenames:
217
+ config_file = search_dir / filename
218
+ if config_file.exists() and config_file.is_file():
219
+ return config_file
220
+
221
+ return None
222
+
223
+ def _load_config_file(self, config_file: Path) -> Dict[str, Any]:
224
+ """Load configuration from file."""
225
+ try:
226
+ import yaml
227
+
228
+ with open(config_file, 'r') as f:
229
+ if config_file.suffix.lower() in ('.yaml', '.yml'):
230
+ return yaml.safe_load(f) or {}
231
+ else:
232
+ # Try JSON as fallback
233
+ import json
234
+ f.seek(0)
235
+ return json.load(f)
236
+
237
+ except Exception as e:
238
+ self.logger.error(f"Failed to load config file {config_file}: {e}")
239
+ return {}
240
+
241
+ def _load_env_config(self, prefix: str) -> Dict[str, Any]:
242
+ """Load configuration from environment variables."""
243
+ config = {}
244
+
245
+ for key, value in os.environ.items():
246
+ if key.startswith(prefix):
247
+ # Convert env var name to config key
248
+ config_key = key[len(prefix):].lower().replace('_', '.')
249
+
250
+ # Try to parse value
251
+ parsed_value = self._parse_env_value(value)
252
+ config[config_key] = parsed_value
253
+
254
+ return config
255
+
256
+ def _parse_env_value(self, value: str) -> Any:
257
+ """Parse environment variable value."""
258
+ # Try boolean
259
+ if value.lower() in ('true', 'false'):
260
+ return value.lower() == 'true'
261
+
262
+ # Try integer
263
+ try:
264
+ return int(value)
265
+ except ValueError:
266
+ pass
267
+
268
+ # Try float
269
+ try:
270
+ return float(value)
271
+ except ValueError:
272
+ pass
273
+
274
+ # Try JSON
275
+ try:
276
+ import json
277
+ return json.loads(value)
278
+ except (json.JSONDecodeError, ValueError):
279
+ pass
280
+
281
+ # Return as string
282
+ return value
283
+
284
+ def _validate_required_keys(self, config: Config, required_keys: List[str]) -> None:
285
+ """Validate that required keys are present."""
286
+ missing_keys = []
287
+
288
+ for key in required_keys:
289
+ if config.get(key) is None:
290
+ missing_keys.append(key)
291
+
292
+ if missing_keys:
293
+ raise ValueError(f"Missing required configuration keys: {missing_keys}")
294
+
295
+ def _generate_cache_key(self, pattern: ConfigPattern) -> str:
296
+ """Generate cache key for pattern."""
297
+ import hashlib
298
+
299
+ # Create a string representation of the pattern
300
+ pattern_str = f"{pattern.filenames}_{pattern.search_paths}_{pattern.env_prefix}"
301
+
302
+ # Generate hash
303
+ return hashlib.md5(pattern_str.encode()).hexdigest()[:8]
304
+
305
+ def clear_cache(self, cache_key: str = None) -> None:
306
+ """Clear configuration cache."""
307
+ if cache_key:
308
+ self._cache.pop(cache_key, None)
309
+ self.logger.debug(f"Cleared cache for: {cache_key}")
310
+ else:
311
+ self._cache.clear()
312
+ self.logger.debug("Cleared all config cache")
313
+
314
+ def get_cache_info(self) -> Dict[str, Any]:
315
+ """Get cache information."""
316
+ return {
317
+ "cached_configs": len(self._cache),
318
+ "cache_keys": list(self._cache.keys()),
319
+ "working_dir": str(self.working_dir)
320
+ }