""" Artifact Management for autonomous-dev v2.0 Handles creation, validation, and reading of workflow artifacts. See error-handling-patterns skill for exception hierarchy and error handling best practices. Design Patterns: See library-design-patterns skill for standardized design patterns. See state-management-patterns skill for standardized design patterns. """ import json from pathlib import Path from datetime import datetime from typing import Dict, Any, Optional, Literal from dataclasses import dataclass @dataclass class ArtifactMetadata: """Metadata for all artifacts""" version: str = "2.0" workflow_id: str = "" agent: str = "" status: Literal["pending", "in_progress", "completed", "failed"] = "pending" created_at: Optional[str] = None updated_at: Optional[str] = None def __post_init__(self): if self.created_at is None: self.created_at = datetime.utcnow().isoformat() if self.updated_at is None: self.updated_at = self.created_at class ArtifactManager: """ Manages workflow artifacts with validation and schema enforcement """ # Required fields for all artifacts REQUIRED_FIELDS = ['version', 'agent', 'workflow_id', 'status'] # Valid artifact types ARTIFACT_TYPES = [ 'manifest', 'research', 'architecture', 'test-plan', 'implementation', 'review', 'security', 'docs', 'final-report' ] def __init__(self, artifacts_dir: Optional[Path] = None): """ Initialize artifact manager Args: artifacts_dir: Base directory for artifacts (default: .claude/artifacts) """ if artifacts_dir is None: artifacts_dir = Path(".claude/artifacts") self.artifacts_dir = artifacts_dir self.artifacts_dir.mkdir(parents=True, exist_ok=True) def create_workflow_directory(self, workflow_id: str) -> Path: """ Create directory for a new workflow Args: workflow_id: Unique workflow identifier Returns: Path to workflow directory """ workflow_dir = self.artifacts_dir / workflow_id workflow_dir.mkdir(parents=True, exist_ok=True) return workflow_dir def get_workflow_directory(self, workflow_id: str) -> Path: """Get path to workflow directory""" return self.artifacts_dir / workflow_id def write_artifact( self, workflow_id: str, artifact_type: str, data: Dict[str, Any], validate: bool = True ) -> Path: """ Write artifact to file Args: workflow_id: Workflow identifier artifact_type: Type of artifact (manifest, research, etc.) data: Artifact data (must include metadata fields) validate: Whether to validate artifact before writing Returns: Path to written artifact file Raises: ValueError: If artifact is invalid """ # Validate artifact type if artifact_type not in self.ARTIFACT_TYPES: raise ValueError( f"Invalid artifact type: {artifact_type}. " f"Valid types: {self.ARTIFACT_TYPES}" ) # Validate artifact data if validate: is_valid, error = self.validate_artifact(data) if not is_valid: raise ValueError(f"Invalid artifact: {error}") # Ensure workflow directory exists workflow_dir = self.create_workflow_directory(workflow_id) # Write artifact artifact_path = workflow_dir / f"{artifact_type}.json" artifact_path.write_text(json.dumps(data, indent=2)) return artifact_path def read_artifact( self, workflow_id: str, artifact_type: str, validate: bool = True ) -> Dict[str, Any]: """ Read artifact from file Args: workflow_id: Workflow identifier artifact_type: Type of artifact validate: Whether to validate artifact after reading Returns: Artifact data Raises: FileNotFoundError: If artifact doesn't exist ValueError: If artifact is invalid """ artifact_path = self.get_workflow_directory(workflow_id) / f"{artifact_type}.json" if not artifact_path.exists(): raise FileNotFoundError(f"Artifact not found: {artifact_path}") data = json.loads(artifact_path.read_text()) if validate: is_valid, error = self.validate_artifact(data) if not is_valid: raise ValueError(f"Invalid artifact: {error}") return data def artifact_exists(self, workflow_id: str, artifact_type: str) -> bool: """Check if artifact exists""" artifact_path = self.get_workflow_directory(workflow_id) / f"{artifact_type}.json" return artifact_path.exists() def list_artifacts(self, workflow_id: str) -> list[str]: """ List all artifacts for a workflow Args: workflow_id: Workflow identifier Returns: List of artifact types (without .json extension) """ workflow_dir = self.get_workflow_directory(workflow_id) if not workflow_dir.exists(): return [] artifacts = [] for artifact_path in workflow_dir.glob("*.json"): artifact_type = artifact_path.stem # Remove .json extension if artifact_type in self.ARTIFACT_TYPES: artifacts.append(artifact_type) return sorted(artifacts) @classmethod def validate_artifact(cls, data: Dict[str, Any]) -> tuple[bool, Optional[str]]: """ Validate artifact has required fields and correct format Args: data: Artifact data to validate Returns: (is_valid, error_message) """ # Check required fields for field in cls.REQUIRED_FIELDS: if field not in data: return False, f"Missing required field: {field}" # Validate version format if not data['version'].startswith('2.'): return False, f"Invalid version: {data['version']} (expected 2.x)" # Validate status values valid_statuses = ['pending', 'in_progress', 'completed', 'failed'] if data['status'] not in valid_statuses: return False, f"Invalid status: {data['status']} (expected: {valid_statuses})" return True, None def create_manifest_artifact( self, workflow_id: str, request: str, alignment_data: Dict[str, Any], workflow_plan: Dict[str, Any] ) -> Path: """ Create workflow manifest artifact (created by orchestrator) Args: workflow_id: Workflow identifier request: User's original request alignment_data: PROJECT.md alignment validation results workflow_plan: Plan for which agents to run and in what order Returns: Path to created manifest """ manifest = { 'version': '2.0', 'agent': 'orchestrator', 'workflow_id': workflow_id, 'status': 'in_progress', 'created_at': datetime.utcnow().isoformat(), 'request': request, 'alignment': alignment_data, 'workflow_plan': workflow_plan } return self.write_artifact(workflow_id, 'manifest', manifest) def get_workflow_summary(self, workflow_id: str) -> Dict[str, Any]: """ Get summary of workflow progress Args: workflow_id: Workflow identifier Returns: Summary with artifact statuses, progress, etc. """ workflow_dir = self.get_workflow_directory(workflow_id) if not workflow_dir.exists(): return {'error': f'Workflow not found: {workflow_id}'} # List all artifacts artifacts = self.list_artifacts(workflow_id) # Get status of each artifact artifact_statuses = {} for artifact_type in artifacts: try: artifact_data = self.read_artifact(workflow_id, artifact_type, validate=False) artifact_statuses[artifact_type] = { 'status': artifact_data.get('status', 'unknown'), 'agent': artifact_data.get('agent', 'unknown'), 'created_at': artifact_data.get('created_at', 'unknown') } except Exception as e: artifact_statuses[artifact_type] = {'error': str(e)} # Calculate overall progress total_expected = len(self.ARTIFACT_TYPES) completed = sum(1 for s in artifact_statuses.values() if s.get('status') == 'completed') progress_percentage = int((completed / total_expected) * 100) return { 'workflow_id': workflow_id, 'artifacts': artifact_statuses, 'total_artifacts': len(artifacts), 'completed': completed, 'progress_percentage': progress_percentage, 'workflow_dir': str(workflow_dir) } def cleanup_old_workflows(self, keep_recent: int = 10): """ Clean up old workflow directories Args: keep_recent: Number of recent workflows to keep """ workflows = sorted( [d for d in self.artifacts_dir.iterdir() if d.is_dir()], key=lambda d: d.stat().st_mtime, reverse=True ) # Delete old workflows for workflow_dir in workflows[keep_recent:]: try: import shutil shutil.rmtree(workflow_dir) except Exception as e: print(f"Warning: Could not delete {workflow_dir}: {e}") def generate_workflow_id() -> str: """ Generate unique workflow identifier Returns: Workflow ID in format: YYYYMMDD_HHMMSS """ return datetime.utcnow().strftime("%Y%m%d_%H%M%S") if __name__ == '__main__': # Example usage import tempfile with tempfile.TemporaryDirectory() as tmpdir: # Create artifact manager manager = ArtifactManager(artifacts_dir=Path(tmpdir)) # Create workflow workflow_id = generate_workflow_id() print(f"Created workflow: {workflow_id}") # Write manifest manifest_path = manager.create_manifest_artifact( workflow_id=workflow_id, request="Implement user authentication", alignment_data={ 'validated': True, 'matches_goals': ['Improve security'], 'within_scope': True }, workflow_plan={ 'agents': ['researcher', 'planner', 'test-master', 'implementer'], 'parallel_validators': ['reviewer', 'security-auditor', 'doc-master'] } ) print(f"Created manifest: {manifest_path}") # Read manifest manifest = manager.read_artifact(workflow_id, 'manifest') print(f"Read manifest: {manifest['request']}") # Get summary summary = manager.get_workflow_summary(workflow_id) print(f"Workflow summary: {json.dumps(summary, indent=2)}")