claude-mpm 3.8.1__py3-none-any.whl → 3.9.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 (33) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +59 -135
  3. claude_mpm/agents/MEMORY.md +39 -30
  4. claude_mpm/agents/WORKFLOW.md +54 -4
  5. claude_mpm/agents/agents_metadata.py +25 -1
  6. claude_mpm/agents/schema/agent_schema.json +1 -1
  7. claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +88 -0
  8. claude_mpm/agents/templates/project_organizer.json +178 -0
  9. claude_mpm/agents/templates/research.json +33 -30
  10. claude_mpm/agents/templates/ticketing.json +3 -3
  11. claude_mpm/cli/commands/agents.py +8 -3
  12. claude_mpm/core/claude_runner.py +31 -10
  13. claude_mpm/core/config.py +2 -2
  14. claude_mpm/core/container.py +96 -25
  15. claude_mpm/core/framework_loader.py +43 -1
  16. claude_mpm/core/interactive_session.py +47 -0
  17. claude_mpm/hooks/claude_hooks/hook_handler_fixed.py +454 -0
  18. claude_mpm/services/agents/deployment/agent_deployment.py +144 -43
  19. claude_mpm/services/agents/memory/agent_memory_manager.py +4 -3
  20. claude_mpm/services/framework_claude_md_generator/__init__.py +10 -3
  21. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +14 -11
  22. claude_mpm/services/response_tracker.py +3 -5
  23. claude_mpm/services/ticket_manager.py +2 -2
  24. claude_mpm/services/ticket_manager_di.py +1 -1
  25. claude_mpm/services/version_control/semantic_versioning.py +80 -7
  26. claude_mpm/services/version_control/version_parser.py +528 -0
  27. claude_mpm-3.9.2.dist-info/METADATA +200 -0
  28. {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.2.dist-info}/RECORD +32 -28
  29. claude_mpm-3.8.1.dist-info/METADATA +0 -327
  30. {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.2.dist-info}/WHEEL +0 -0
  31. {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.2.dist-info}/entry_points.txt +0 -0
  32. {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.2.dist-info}/licenses/LICENSE +0 -0
  33. {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,528 @@
1
+ """
2
+ Enhanced version parsing system with multiple source support and fallback mechanisms.
3
+
4
+ This module provides a robust version parsing system that can retrieve version information
5
+ from multiple sources with intelligent fallback logic:
6
+
7
+ 1. Git tags (primary source - most reliable)
8
+ 2. CHANGELOG.md (for release notes and history)
9
+ 3. VERSION file (for current version)
10
+ 4. package.json (for npm packages)
11
+ 5. pyproject.toml (for Python packages)
12
+
13
+ The system includes caching for performance and validation for data integrity.
14
+ """
15
+
16
+ import json
17
+ import re
18
+ import subprocess
19
+ from datetime import datetime, timedelta
20
+ from pathlib import Path
21
+ from typing import Dict, List, Optional, Tuple, Union
22
+ from functools import lru_cache
23
+ import logging
24
+
25
+ from claude_mpm.services.version_control.semantic_versioning import SemanticVersion
26
+
27
+
28
+ class VersionSource:
29
+ """Enumeration of version sources with priority ordering."""
30
+ GIT_TAGS = "git_tags"
31
+ CHANGELOG = "changelog"
32
+ VERSION_FILE = "version_file"
33
+ PACKAGE_JSON = "package_json"
34
+ PYPROJECT_TOML = "pyproject_toml"
35
+ SETUP_PY = "setup_py"
36
+
37
+ # Priority order for fallback mechanism
38
+ PRIORITY_ORDER = [
39
+ GIT_TAGS,
40
+ CHANGELOG,
41
+ VERSION_FILE,
42
+ PACKAGE_JSON,
43
+ PYPROJECT_TOML,
44
+ SETUP_PY
45
+ ]
46
+
47
+
48
+ class VersionMetadata:
49
+ """Extended metadata for version information."""
50
+
51
+ def __init__(
52
+ self,
53
+ version: str,
54
+ source: str,
55
+ release_date: Optional[datetime] = None,
56
+ commit_hash: Optional[str] = None,
57
+ author: Optional[str] = None,
58
+ message: Optional[str] = None,
59
+ changes: Optional[List[str]] = None
60
+ ):
61
+ self.version = version
62
+ self.source = source
63
+ self.release_date = release_date or datetime.now()
64
+ self.commit_hash = commit_hash
65
+ self.author = author
66
+ self.message = message
67
+ self.changes = changes or []
68
+
69
+ def to_dict(self) -> Dict:
70
+ """Convert metadata to dictionary format."""
71
+ return {
72
+ "version": self.version,
73
+ "source": self.source,
74
+ "release_date": self.release_date.isoformat() if self.release_date else None,
75
+ "commit_hash": self.commit_hash,
76
+ "author": self.author,
77
+ "message": self.message,
78
+ "changes": self.changes
79
+ }
80
+
81
+
82
+ class EnhancedVersionParser:
83
+ """
84
+ Enhanced version parser with multiple source support and intelligent fallback.
85
+
86
+ This parser provides:
87
+ - Multiple version source support
88
+ - Intelligent fallback mechanisms
89
+ - Caching for performance
90
+ - Validation and error handling
91
+ - Comprehensive version history retrieval
92
+ """
93
+
94
+ def __init__(self, project_root: Optional[Path] = None, cache_ttl: int = 300):
95
+ """
96
+ Initialize the enhanced version parser.
97
+
98
+ Args:
99
+ project_root: Root directory of the project (defaults to current directory)
100
+ cache_ttl: Cache time-to-live in seconds (default: 5 minutes)
101
+ """
102
+ self.project_root = project_root or Path.cwd()
103
+ self.cache_ttl = cache_ttl
104
+ self.logger = logging.getLogger(__name__)
105
+ self._cache: Dict[str, Tuple[datetime, any]] = {}
106
+
107
+ # Compile regex patterns once for efficiency
108
+ self._version_pattern = re.compile(r'(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9\-\.]+))?(?:\+([a-zA-Z0-9\-\.]+))?')
109
+ self._changelog_version_pattern = re.compile(r'##\s*\[?([0-9]+\.[0-9]+\.[0-9]+[^\]]*)\]?\s*[-–]\s*(\d{4}-\d{2}-\d{2})?')
110
+
111
+ def _get_cached(self, key: str) -> Optional[any]:
112
+ """Get cached value if still valid."""
113
+ if key in self._cache:
114
+ timestamp, value = self._cache[key]
115
+ if datetime.now() - timestamp < timedelta(seconds=self.cache_ttl):
116
+ return value
117
+ del self._cache[key]
118
+ return None
119
+
120
+ def _set_cached(self, key: str, value: any) -> any:
121
+ """Set cached value with timestamp."""
122
+ self._cache[key] = (datetime.now(), value)
123
+ return value
124
+
125
+ def get_current_version(self, prefer_source: Optional[str] = None) -> Optional[VersionMetadata]:
126
+ """
127
+ Get the current version from the most reliable available source.
128
+
129
+ Args:
130
+ prefer_source: Preferred source to check first (optional)
131
+
132
+ Returns:
133
+ VersionMetadata with current version information, or None if not found
134
+ """
135
+ cache_key = f"current_version_{prefer_source or 'auto'}"
136
+ cached = self._get_cached(cache_key)
137
+ if cached:
138
+ return cached
139
+
140
+ sources = VersionSource.PRIORITY_ORDER.copy()
141
+ if prefer_source and prefer_source in sources:
142
+ sources.remove(prefer_source)
143
+ sources.insert(0, prefer_source)
144
+
145
+ for source in sources:
146
+ try:
147
+ version = self._get_version_from_source(source, latest_only=True)
148
+ if version:
149
+ return self._set_cached(cache_key, version)
150
+ except Exception as e:
151
+ self.logger.debug(f"Failed to get version from {source}: {e}")
152
+
153
+ return None
154
+
155
+ def get_version_history(
156
+ self,
157
+ include_prereleases: bool = False,
158
+ limit: Optional[int] = None
159
+ ) -> List[VersionMetadata]:
160
+ """
161
+ Get complete version history from all available sources.
162
+
163
+ Args:
164
+ include_prereleases: Include pre-release versions (alpha, beta, rc)
165
+ limit: Maximum number of versions to return
166
+
167
+ Returns:
168
+ List of VersionMetadata objects sorted by version (descending)
169
+ """
170
+ cache_key = f"version_history_{include_prereleases}_{limit}"
171
+ cached = self._get_cached(cache_key)
172
+ if cached:
173
+ return cached
174
+
175
+ all_versions: Dict[str, VersionMetadata] = {}
176
+
177
+ # Try each source and merge results
178
+ for source in VersionSource.PRIORITY_ORDER:
179
+ try:
180
+ versions = self._get_versions_from_source(source)
181
+ for version in versions:
182
+ # Use the first occurrence of each version (highest priority source)
183
+ if version.version not in all_versions:
184
+ all_versions[version.version] = version
185
+ except Exception as e:
186
+ self.logger.debug(f"Failed to get versions from {source}: {e}")
187
+
188
+ # Filter and sort versions
189
+ result = list(all_versions.values())
190
+
191
+ if not include_prereleases:
192
+ result = [v for v in result if not self._is_prerelease(v.version)]
193
+
194
+ # Sort by semantic version
195
+ result.sort(key=lambda v: self._parse_semver(v.version), reverse=True)
196
+
197
+ if limit:
198
+ result = result[:limit]
199
+
200
+ return self._set_cached(cache_key, result)
201
+
202
+ def _get_version_from_source(
203
+ self,
204
+ source: str,
205
+ latest_only: bool = False
206
+ ) -> Optional[VersionMetadata]:
207
+ """Get version(s) from a specific source."""
208
+ if source == VersionSource.GIT_TAGS:
209
+ return self._get_version_from_git(latest_only)
210
+ elif source == VersionSource.VERSION_FILE:
211
+ return self._get_version_from_file()
212
+ elif source == VersionSource.PACKAGE_JSON:
213
+ return self._get_version_from_package_json()
214
+ elif source == VersionSource.PYPROJECT_TOML:
215
+ return self._get_version_from_pyproject()
216
+ elif source == VersionSource.CHANGELOG:
217
+ versions = self._get_versions_from_changelog()
218
+ return versions[0] if versions else None
219
+ return None
220
+
221
+ def _get_versions_from_source(self, source: str) -> List[VersionMetadata]:
222
+ """Get all versions from a specific source."""
223
+ if source == VersionSource.GIT_TAGS:
224
+ return self._get_all_versions_from_git()
225
+ elif source == VersionSource.CHANGELOG:
226
+ return self._get_versions_from_changelog()
227
+ elif source in [VersionSource.VERSION_FILE, VersionSource.PACKAGE_JSON, VersionSource.PYPROJECT_TOML]:
228
+ # These sources only provide current version
229
+ version = self._get_version_from_source(source, latest_only=True)
230
+ return [version] if version else []
231
+ return []
232
+
233
+ def _get_version_from_git(self, latest_only: bool = True) -> Optional[VersionMetadata]:
234
+ """Get version information from git tags."""
235
+ try:
236
+ if latest_only:
237
+ # Get the latest tag
238
+ result = subprocess.run(
239
+ ["git", "describe", "--tags", "--abbrev=0"],
240
+ capture_output=True,
241
+ text=True,
242
+ cwd=self.project_root,
243
+ check=False
244
+ )
245
+ if result.returncode == 0:
246
+ tag = result.stdout.strip()
247
+ return self._parse_git_tag(tag)
248
+ else:
249
+ return self._get_all_versions_from_git()[0] if self._get_all_versions_from_git() else None
250
+ except Exception as e:
251
+ self.logger.debug(f"Failed to get git version: {e}")
252
+ return None
253
+
254
+ def _get_all_versions_from_git(self) -> List[VersionMetadata]:
255
+ """Get all versions from git tags with metadata."""
256
+ versions = []
257
+ try:
258
+ # Get all tags with dates and messages
259
+ result = subprocess.run(
260
+ ["git", "for-each-ref", "--sort=-version:refname", "--format=%(refname:short)|%(creatordate:iso)|%(subject)", "refs/tags"],
261
+ capture_output=True,
262
+ text=True,
263
+ cwd=self.project_root,
264
+ check=False
265
+ )
266
+
267
+ if result.returncode == 0:
268
+ for line in result.stdout.strip().split('\n'):
269
+ if line:
270
+ parts = line.split('|', 2)
271
+ if len(parts) >= 1:
272
+ tag = parts[0]
273
+ date_str = parts[1] if len(parts) > 1 else None
274
+ message = parts[2] if len(parts) > 2 else None
275
+
276
+ # Parse the tag
277
+ metadata = self._parse_git_tag(tag, date_str, message)
278
+ if metadata:
279
+ versions.append(metadata)
280
+ except Exception as e:
281
+ self.logger.debug(f"Failed to get git versions: {e}")
282
+
283
+ return versions
284
+
285
+ def _parse_git_tag(
286
+ self,
287
+ tag: str,
288
+ date_str: Optional[str] = None,
289
+ message: Optional[str] = None
290
+ ) -> Optional[VersionMetadata]:
291
+ """Parse a git tag into VersionMetadata."""
292
+ # Remove 'v' prefix if present
293
+ version = tag[1:] if tag.startswith('v') else tag
294
+
295
+ # Validate version format
296
+ if not self._version_pattern.match(version):
297
+ return None
298
+
299
+ # Parse date if provided
300
+ release_date = None
301
+ if date_str:
302
+ try:
303
+ release_date = datetime.fromisoformat(date_str.replace(' ', 'T'))
304
+ except:
305
+ pass
306
+
307
+ # Get commit hash for this tag
308
+ commit_hash = None
309
+ try:
310
+ result = subprocess.run(
311
+ ["git", "rev-list", "-n", "1", tag],
312
+ capture_output=True,
313
+ text=True,
314
+ cwd=self.project_root,
315
+ check=False
316
+ )
317
+ if result.returncode == 0:
318
+ commit_hash = result.stdout.strip()[:7]
319
+ except:
320
+ pass
321
+
322
+ return VersionMetadata(
323
+ version=version,
324
+ source=VersionSource.GIT_TAGS,
325
+ release_date=release_date,
326
+ commit_hash=commit_hash,
327
+ message=message
328
+ )
329
+
330
+ def _get_version_from_file(self) -> Optional[VersionMetadata]:
331
+ """Get version from VERSION file."""
332
+ version_file = self.project_root / "VERSION"
333
+ if version_file.exists():
334
+ try:
335
+ version = version_file.read_text().strip()
336
+ if self._version_pattern.match(version):
337
+ return VersionMetadata(
338
+ version=version,
339
+ source=VersionSource.VERSION_FILE
340
+ )
341
+ except Exception as e:
342
+ self.logger.debug(f"Failed to read VERSION file: {e}")
343
+ return None
344
+
345
+ def _get_version_from_package_json(self) -> Optional[VersionMetadata]:
346
+ """Get version from package.json."""
347
+ package_file = self.project_root / "package.json"
348
+ if package_file.exists():
349
+ try:
350
+ with open(package_file) as f:
351
+ data = json.load(f)
352
+ version = data.get("version")
353
+ if version and self._version_pattern.match(version):
354
+ return VersionMetadata(
355
+ version=version,
356
+ source=VersionSource.PACKAGE_JSON
357
+ )
358
+ except Exception as e:
359
+ self.logger.debug(f"Failed to read package.json: {e}")
360
+ return None
361
+
362
+ def _get_version_from_pyproject(self) -> Optional[VersionMetadata]:
363
+ """Get version from pyproject.toml."""
364
+ pyproject_file = self.project_root / "pyproject.toml"
365
+ if pyproject_file.exists():
366
+ try:
367
+ content = pyproject_file.read_text()
368
+ # Look for version in [tool.poetry] or [project] sections
369
+ patterns = [
370
+ r'version\s*=\s*["\']([^"\']+)["\']',
371
+ r'version\s*=\s*\{[^}]*\}', # Dynamic version
372
+ ]
373
+
374
+ for pattern in patterns:
375
+ match = re.search(pattern, content)
376
+ if match:
377
+ version = match.group(1) if match.lastindex else None
378
+ if version and self._version_pattern.match(version):
379
+ return VersionMetadata(
380
+ version=version,
381
+ source=VersionSource.PYPROJECT_TOML
382
+ )
383
+ except Exception as e:
384
+ self.logger.debug(f"Failed to read pyproject.toml: {e}")
385
+ return None
386
+
387
+ def _get_versions_from_changelog(self) -> List[VersionMetadata]:
388
+ """Parse version history from CHANGELOG.md."""
389
+ versions = []
390
+ changelog_paths = [
391
+ self.project_root / "CHANGELOG.md",
392
+ self.project_root / "docs" / "CHANGELOG.md",
393
+ self.project_root / "HISTORY.md"
394
+ ]
395
+
396
+ for changelog_path in changelog_paths:
397
+ if changelog_path.exists():
398
+ try:
399
+ content = changelog_path.read_text()
400
+
401
+ # Find all version entries
402
+ for match in self._changelog_version_pattern.finditer(content):
403
+ version = match.group(1).strip()
404
+ date_str = match.group(2) if match.lastindex >= 2 else None
405
+
406
+ # Parse release date
407
+ release_date = None
408
+ if date_str:
409
+ try:
410
+ release_date = datetime.strptime(date_str, "%Y-%m-%d")
411
+ except:
412
+ pass
413
+
414
+ # Extract changes for this version
415
+ changes = self._extract_changelog_changes(content, match.start())
416
+
417
+ versions.append(VersionMetadata(
418
+ version=version,
419
+ source=VersionSource.CHANGELOG,
420
+ release_date=release_date,
421
+ changes=changes
422
+ ))
423
+
424
+ if versions:
425
+ break
426
+ except Exception as e:
427
+ self.logger.debug(f"Failed to parse changelog: {e}")
428
+
429
+ return versions
430
+
431
+ def _extract_changelog_changes(self, content: str, start_pos: int) -> List[str]:
432
+ """Extract change entries for a specific version from changelog."""
433
+ changes = []
434
+ lines = content[start_pos:].split('\n')
435
+
436
+ in_changes = False
437
+ for line in lines[1:]: # Skip the version header line
438
+ # Stop at next version header
439
+ if line.startswith('##'):
440
+ break
441
+
442
+ # Collect change lines (usually start with -, *, or +)
443
+ if line.strip().startswith(('-', '*', '+')):
444
+ changes.append(line.strip()[1:].strip())
445
+ in_changes = True
446
+ elif in_changes and line.strip() and not line.startswith('#'):
447
+ # Continuation of previous change
448
+ if changes:
449
+ changes[-1] += ' ' + line.strip()
450
+
451
+ return changes
452
+
453
+ def _is_prerelease(self, version: str) -> bool:
454
+ """Check if a version is a pre-release."""
455
+ prerelease_patterns = [
456
+ r'-(?:alpha|beta|rc|dev|pre)',
457
+ r'\.(?:alpha|beta|rc|dev|pre)',
458
+ r'(?:a|b|rc)\d+$'
459
+ ]
460
+
461
+ for pattern in prerelease_patterns:
462
+ if re.search(pattern, version, re.IGNORECASE):
463
+ return True
464
+ return False
465
+
466
+ def _parse_semver(self, version: str) -> Tuple[int, int, int, str, str]:
467
+ """
468
+ Parse semantic version for sorting.
469
+
470
+ Returns tuple of (major, minor, patch, prerelease, build)
471
+ """
472
+ match = self._version_pattern.match(version)
473
+ if match:
474
+ major = int(match.group(1))
475
+ minor = int(match.group(2))
476
+ patch = int(match.group(3))
477
+ prerelease = match.group(4) or ''
478
+ build = match.group(5) or ''
479
+ return (major, minor, patch, prerelease, build)
480
+ return (0, 0, 0, '', '')
481
+
482
+ def validate_version_consistency(self) -> Dict[str, str]:
483
+ """
484
+ Validate version consistency across all sources.
485
+
486
+ Returns:
487
+ Dictionary mapping source names to versions found
488
+ """
489
+ versions = {}
490
+
491
+ for source in VersionSource.PRIORITY_ORDER:
492
+ try:
493
+ version = self._get_version_from_source(source, latest_only=True)
494
+ if version:
495
+ versions[source] = version.version
496
+ except Exception as e:
497
+ self.logger.debug(f"Failed to check {source}: {e}")
498
+
499
+ return versions
500
+
501
+ def get_version_for_release(self) -> Optional[str]:
502
+ """
503
+ Get the version that should be used for the next release.
504
+
505
+ This prioritizes git tags as the source of truth, falling back
506
+ to VERSION file if no git tags exist.
507
+
508
+ Returns:
509
+ Version string for release, or None if no version found
510
+ """
511
+ # Try git first
512
+ git_version = self._get_version_from_git(latest_only=True)
513
+ if git_version:
514
+ return git_version.version
515
+
516
+ # Fall back to VERSION file
517
+ file_version = self._get_version_from_file()
518
+ if file_version:
519
+ return file_version.version
520
+
521
+ return None
522
+
523
+
524
+ # Convenience function for backward compatibility
525
+ @lru_cache(maxsize=1)
526
+ def get_version_parser(project_root: Optional[Path] = None) -> EnhancedVersionParser:
527
+ """Get a singleton instance of the version parser."""
528
+ return EnhancedVersionParser(project_root)