""" Main orchestrator for AI agent execution. Executes a single task per run: read task -> execute -> test -> commit """ import sys import subprocess import time import json import re import urllib.request import urllib.error from pathlib import Path from datetime import datetime, timezone from typing import Optional, List, Tuple from config import AgentConfig, get_test_command, get_lint_command from task_manager import TaskManager, GlobalLock, Task from cursor_cli_wrapper import CursorCLI, CursorResultStatus from planning_agent import PlanningAgent class AgentOrchestrator: """Orchestrates the agent development cycle.""" def __init__(self, config: AgentConfig): self.config = config self.task_manager = TaskManager( config.task_list_path, config.agent_state_path ) self.global_lock = GlobalLock(config.global_lock_path) self.cursor_cli = CursorCLI( project_root=config.project_root, api_key=config.cursor_api_key, model=config.cursor_model, verbose=True ) def run(self) -> int: """ Execute a single task from the task list. Returns: 0 on success, 1 on error """ print(f"๐Ÿš€ Starting AI Agent for component: {self.config.component}") print(f"๐Ÿ“ Project root: {self.config.project_root}") print(f"๐ŸŽฏ Executing one task per run") # Check if another agent is running if self.task_manager.is_agent_running(): print(f"โš ๏ธ Another agent is already running for {self.config.component}") print("Exiting to avoid conflicts.") return 0 try: state = self.task_manager.load_state() # Reset stale in_progress tasks (tasks that were left in_progress from previous runs) self.task_manager.reset_stale_in_progress_tasks(state.current_task_id) # Check iteration limit if state.iteration_count >= self.config.max_iterations: print(f"\nโน๏ธ Reached maximum iterations ({self.config.max_iterations})") return 0 # Get next task next_task = self.task_manager.get_next_task() if not next_task: print("\nโœ… All tasks completed or no pending tasks!") # Trigger planning agent to generate new tasks print("\n๐ŸŽฏ No tasks available, triggering planning agent...") planning_agent = PlanningAgent(self.config) planning_result = planning_agent.run() if planning_result == 0: print("\nโœ… Planning agent completed successfully") print(" New tasks have been generated. Checking for available tasks...") # Try to get next task again after planning next_task = self.task_manager.get_next_task() if next_task: print(f"\n๐Ÿ“‹ Found new task: {next_task.id} - {next_task.title}") print(" Proceeding to execute the new task...") # Continue with task execution (don't return, fall through to task execution) else: print("\nโš ๏ธ Planning agent completed but no new tasks were generated") print(" Agent will exit. You may need to manually create tasks.") self.task_manager.reset_state() return 0 else: print("\nโš ๏ธ Planning agent failed") print(" Agent will exit. You may need to manually trigger planning or create tasks") self.task_manager.reset_state() return 0 # At this point we should have a task to execute # Continue with task execution print(f"\n{'='*80}") print(f"๐Ÿ“‹ Task: {next_task.id}") print(f"๐Ÿ“ {next_task.title}") print(f"โฑ๏ธ Estimated: {next_task.estimated_hours}h") print(f"{'='*80}\n") # Start task (only if not already in progress) if next_task.status != "in_progress": self.task_manager.start_task(next_task) # Check if task needs global lock (modifies common package) needs_global_lock = self._needs_global_lock(next_task) if needs_global_lock: if not self._acquire_global_lock(): print("โš ๏ธ Cannot acquire global lock, skipping task") self.task_manager.mark_task_skipped( next_task.id, "Global lock not available" ) return 0 try: # Retry loop: keep trying the same task until success or max retries while True: # Get current retry count current_state = self.task_manager.load_state() retry_count = current_state.retry_count if retry_count > 0: print(f"\n๐Ÿ”„ Retry attempt {retry_count}/{self.config.max_retries} for task {next_task.id}") # Execute task success = self._execute_task(next_task) if success: # Task completed successfully print(f"\nโœ… Task {next_task.id} completed successfully") self.task_manager.mark_task_completed(next_task.id) break else: # Task failed, increment retry count retry_count = self.task_manager.increment_retry() if retry_count >= self.config.max_retries: print(f"\nโŒ Task {next_task.id} failed after {retry_count} retries") # Try to break down the task into subtasks print(f"\n๐Ÿ”€ Attempting to break down task {next_task.id} into subtasks...") planning_agent = PlanningAgent(self.config) failure_reason = f"Failed after {retry_count} retry attempts. Task may be too complex." breakdown_success = planning_agent.break_down_task( next_task, failure_reason ) if breakdown_success: print(f"\nโœ… Task {next_task.id} successfully broken down into subtasks") print(" Subtasks have been added to task_list.json") # Mark original task as failed self.task_manager.mark_task_failed( next_task.id, f"Failed after {retry_count} retries. Broken down into subtasks." ) else: print(f"\nโš ๏ธ Failed to break down task {next_task.id}") print(" Marking task as failed without subtasks") self.task_manager.mark_task_failed( next_task.id, f"Failed after {retry_count} retries" ) # Create GitHub issue self._create_issue_for_failed_task(next_task) break else: print(f"\nโš ๏ธ Task {next_task.id} failed, retrying ({retry_count}/{self.config.max_retries})") print("Waiting before retry...") time.sleep(5) # Small delay before retry finally: # Release global lock if held if needs_global_lock: self.global_lock.release(self.config.component) print("\n๐ŸŽ‰ Agent execution completed (one task)") return 0 except KeyboardInterrupt: print("\nโš ๏ธ Interrupted by user") self.task_manager.reset_state() return 1 except Exception as e: print(f"\nโŒ Fatal error: {e}") import traceback traceback.print_exc() return 1 def _needs_global_lock(self, task: Task) -> bool: """Check if task needs global lock (modifies shared resources).""" # Check if any files to modify are in mnemo_cards_common for file_path in task.files_to_modify: if "mnemo_cards_common" in file_path: return True return False def _acquire_global_lock(self, timeout_minutes: int = 30) -> bool: """Try to acquire global lock with timeout.""" start_time = time.time() while True: if self.global_lock.acquire( self.config.component, f"Working on {self.config.component}" ): print("๐Ÿ”’ Acquired global lock") return True elapsed_minutes = (time.time() - start_time) / 60 if elapsed_minutes >= timeout_minutes: return False print(f"โณ Waiting for global lock... ({elapsed_minutes:.1f}/{timeout_minutes} min)") time.sleep(30) # Check every 30 seconds def _execute_task(self, task: Task) -> bool: """ Execute a single task. Returns: True if successful, False otherwise """ # Load prompt template prompt = self._build_task_prompt(task) # Run cursor agent print("๐Ÿค– Running Cursor Agent...") result = self.cursor_cli.run_agent( task_description=prompt, force=True, max_iterations=5 ) if result.status != CursorResultStatus.SUCCESS: print(f"โŒ Cursor agent failed: {result.error}") return False print(f"\n๐Ÿ“Š Agent completed:") print(f" - Files created: {len(result.files_created)}") print(f" - Files modified: {len(result.files_modified)}") print(f" - Files read: {len(result.files_read)}") print(f" - Tool calls: {result.tool_calls}") # Check if any files were actually changed if len(result.files_created) == 0 and len(result.files_modified) == 0: print("\nโš ๏ธ WARNING: Agent completed but no files were created or modified!") print(f" Agent read {len(result.files_read)} file(s) but made no changes.") print(" This might indicate:") print(" - Task was already completed") print(" - Agent only analyzed files without making changes") print(" - Agent encountered an issue but didn't report it") # Check git status to see if there are any uncommitted changes git_changes = self._check_git_changes() if git_changes: print(f"\n However, git shows {len(git_changes)} uncommitted change(s):") for change in git_changes[:5]: # Show first 5 print(f" - {change}") if len(git_changes) > 5: print(f" ... and {len(git_changes) - 5} more") else: print("\n Git confirms: no changes detected in working directory.") print(" Task may need to be retried or task description clarified.") # Pull latest changes before committing print("\n๐Ÿ”„ Pulling latest changes...") self._git_pull() # Run linter print("๐Ÿ” Linter is disabled for now") # print("\n๐Ÿ” Running linter...") # if not self._run_lint(): # print("โš ๏ธ Linter found issues") # # Don't fail, cursor can fix in retry # return False # Run tests print("Tests are disabled for now") # print("\n๐Ÿงช Running tests...") # if not self._run_tests(): # print("โŒ Tests failed") # return False # Commit changes print("\n๐Ÿ’พ Committing changes...") commit_message = self._build_commit_message(task, result) if not self._git_commit(commit_message): print("โš ๏ธ No changes to commit") # Push changes (with pull before push to avoid conflicts) # If push fails, _git_push() will automatically create a PR print("\n๐Ÿ“ค Pushing changes...") push_success = self._git_push() if not push_success: print("โš ๏ธ Push failed, attempting to create PR...") # _git_push() already handles PR creation, but if it still failed, # we'll continue anyway as the changes are committed locally print("โš ๏ธ Changes are committed locally but not pushed") print(" You may need to manually push or create a PR") return True def _build_task_prompt(self, task: Task) -> str: """Build comprehensive prompt for the agent.""" # Load development prompt template prompt_file = self.config.prompts_dir / "development_prompt.md" if prompt_file.exists(): with open(prompt_file, 'r') as f: template = f.read() else: template = "You are an AI software engineer. Complete the following task:\n\n" # Add task details prompt = f"""{template} ## TASK: {task.id} - {task.title} ### Priority: {task.priority.upper()} ### Description: {task.description} ### Acceptance Criteria: """ for i, criterion in enumerate(task.acceptance_criteria, 1): prompt += f"{i}. {criterion}\n" prompt += f""" ### Files to Modify: """ for file_path in task.files_to_modify: prompt += f"- {file_path}\n" prompt += f""" ### Component: {task.component} ### Working Directory: {self.config.component_root} --- Complete this task following clean architecture principles and project conventions. Write comprehensive unit tests for all new functionality. Make sure all existing tests continue to pass. """ return prompt def _build_commit_message(self, task: Task, result) -> str: """Build descriptive commit message.""" message = f"""feat({self.config.component}): {task.title} Task ID: {task.id} Priority: {task.priority} Changes: """ if result.files_created: message += f"- Created {len(result.files_created)} file(s)\n" for file in result.files_created[:5]: # Limit to 5 message += f" - {file}\n" if result.files_modified: message += f"- Modified {len(result.files_modified)} file(s)\n" for file in result.files_modified[:5]: # Limit to 5 message += f" - {file}\n" message += f"\nCompleted by: AI Agent\n" message += f"Duration: {result.duration_ms}ms\n" return message def _run_lint(self) -> bool: """Run linter for component.""" cmd = get_lint_command(self.config.component) result = subprocess.run( cmd, shell=True, cwd=self.config.project_root, capture_output=True, text=True ) if result.returncode == 0: print("โœ… Linter passed") return True else: print(f"โš ๏ธ Linter output:\n{result.stdout}") return False def _run_tests(self) -> bool: """Run tests for component.""" cmd = get_test_command(self.config.component) result = subprocess.run( cmd, shell=True, cwd=self.config.project_root, capture_output=True, text=True ) if result.returncode == 0: print("โœ… All tests passed") return True else: print(f"โŒ Test output:\n{result.stdout}") return False def _check_git_changes(self) -> List[str]: """Check for uncommitted changes in git.""" # Check status for modified, added, deleted files result = subprocess.run( ["git", "status", "--porcelain"], cwd=self.config.project_root, capture_output=True, text=True ) if result.returncode != 0: return [] changes = [] for line in result.stdout.strip().split('\n'): if line.strip(): # Format: "XY filename" where X is staging area, Y is working tree # We care about files that are modified (M), added (A), deleted (D), or renamed (R) status = line[:2] filename = line[3:].strip() if any(c in status for c in ['M', 'A', 'D', 'R']): changes.append(filename) return changes def _git_pull(self) -> bool: """Pull latest changes from remote.""" result = subprocess.run( ["git", "pull", "--rebase"], cwd=self.config.project_root, capture_output=True, text=True ) return result.returncode == 0 def _git_commit(self, message: str) -> bool: """Commit changes.""" # Add all changes subprocess.run( ["git", "add", "-A"], cwd=self.config.project_root ) # Commit result = subprocess.run( ["git", "commit", "-m", message], cwd=self.config.project_root, capture_output=True, text=True ) if result.returncode == 0: # Get commit hash commit_hash = subprocess.run( ["git", "rev-parse", "HEAD"], cwd=self.config.project_root, capture_output=True, text=True ).stdout.strip() # Update state with commit hash state = self.task_manager.load_state() state.last_commit = commit_hash self.task_manager.save_state(state) print(f"โœ… Committed: {commit_hash[:8]}") return True else: # No changes or error return False def _get_git_repo_info(self) -> Tuple[Optional[str], Optional[str]]: """Get repository owner and name from git remote.""" result = subprocess.run( ["git", "remote", "get-url", "origin"], cwd=self.config.project_root, capture_output=True, text=True ) if result.returncode != 0: return None, None remote_url = result.stdout.strip() # Parse different URL formats: # https://forgejo.example.com/owner/repo.git # https://forgejo.example.com/owner/repo # git@forgejo.example.com:owner/repo.git # https://github.com/owner/repo.git patterns = [ r'https?://[^/]+/([^/]+)/([^/]+?)(?:\.git)?/?$', r'git@[^:]+:([^/]+)/([^/]+?)(?:\.git)?$', ] for pattern in patterns: match = re.search(pattern, remote_url) if match: owner = match.group(1) repo = match.group(2) return owner, repo return None, None def _get_forgejo_api_url(self) -> Optional[str]: """Get Forgejo/GitHub API URL from git remote.""" if self.config.forgejo_api_url: return self.config.forgejo_api_url result = subprocess.run( ["git", "remote", "get-url", "origin"], cwd=self.config.project_root, capture_output=True, text=True ) if result.returncode != 0: return None remote_url = result.stdout.strip() # Extract base URL # https://forgejo.example.com/owner/repo.git -> https://forgejo.example.com # https://github.com/owner/repo.git -> https://api.github.com # git@forgejo.example.com:owner/repo.git -> https://forgejo.example.com if 'github.com' in remote_url: return 'https://api.github.com' # For Forgejo, extract the base URL match = re.search(r'https?://([^/]+)', remote_url) if match: base_url = f"https://{match.group(1)}" return f"{base_url}/api/v1" match = re.search(r'git@([^:]+)', remote_url) if match: base_url = f"https://{match.group(1)}" return f"{base_url}/api/v1" return None def _check_existing_pr(self, branch_name: str) -> Optional[str]: """Check if PR already exists for this branch. Returns PR URL if found.""" if not self.config.forgejo_token: return None owner, repo = self._get_git_repo_info() if not owner or not repo: return None api_url = self._get_forgejo_api_url() if not api_url: return None # List open PRs for this branch pr_url = f"{api_url}/repos/{owner}/{repo}/pulls?head={owner}:{branch_name}&state=open" try: req = urllib.request.Request( pr_url, headers={ "Authorization": f"token {self.config.forgejo_token}", "Accept": "application/json" } ) with urllib.request.urlopen(req) as response: if response.status == 200: prs = json.loads(response.read().decode('utf-8')) if prs and len(prs) > 0: return prs[0].get('html_url') except Exception: # If check fails, continue anyway pass return None def _create_pull_request(self, task: Task, branch_name: str) -> bool: """Create a pull request via Forgejo/GitHub API.""" if not self.config.forgejo_token: print("โš ๏ธ FORGEJO_TOKEN not set, cannot create PR") return False owner, repo = self._get_git_repo_info() if not owner or not repo: print("โš ๏ธ Could not determine repository owner/name from git remote") return False api_url = self._get_forgejo_api_url() if not api_url: print("โš ๏ธ Could not determine API URL from git remote") return False # Check if PR already exists existing_pr = self._check_existing_pr(branch_name) if existing_pr: print(f"โœ… Pull Request already exists: {existing_pr}") return True # Get current branch name (should match branch_name, but verify) branch_result = subprocess.run( ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=self.config.project_root, capture_output=True, text=True ) if branch_result.returncode != 0: print("โš ๏ธ Could not determine current branch") return False current_branch = branch_result.stdout.strip() # Use the provided branch_name, not current_branch (they should match) if current_branch != branch_name: print(f"โš ๏ธ Branch mismatch: current={current_branch}, expected={branch_name}") # Use current_branch for PR creation branch_name = current_branch # PR title and body pr_title = f"feat({self.config.component}): {task.title}" pr_body = f"""## Task: {task.id} **Priority:** {task.priority} ### Description {task.description} ### Changes This PR contains changes for task {task.id} completed by AI Agent. ### Acceptance Criteria """ for i, criterion in enumerate(task.acceptance_criteria, 1): pr_body += f"{i}. {criterion}\n" pr_body += f""" --- *Created automatically by AI Agent* *Component: {self.config.component}* """ # Create PR via API pr_url = f"{api_url}/repos/{owner}/{repo}/pulls" data = { "title": pr_title, "body": pr_body, "head": current_branch, "base": "master" } try: req = urllib.request.Request( pr_url, data=json.dumps(data).encode('utf-8'), headers={ "Authorization": f"token {self.config.forgejo_token}", "Content-Type": "application/json", "Accept": "application/json" } ) with urllib.request.urlopen(req) as response: if response.status in [200, 201]: result = json.loads(response.read().decode('utf-8')) pr_number = result.get('number', 'unknown') pr_html_url = result.get('html_url', pr_url) print(f"โœ… Created Pull Request #{pr_number}") print(f" URL: {pr_html_url}") return True else: error_body = response.read().decode('utf-8') print(f"โŒ Failed to create PR: HTTP {response.status}") print(f" Response: {error_body}") return False except urllib.error.HTTPError as e: error_body = e.read().decode('utf-8') print(f"โŒ Failed to create PR: HTTP {e.code}") print(f" Response: {error_body}") return False except Exception as e: print(f"โŒ Failed to create PR: {e}") return False def _create_branch_and_pr(self, task: Task) -> bool: """Create a feature branch and PR for the task.""" # Get current task if not provided if not task: state = self.task_manager.load_state() if not state.current_task_id: print("โš ๏ธ No current task ID, cannot create PR") return False tasks = self.task_manager.load_tasks() task = next((t for t in tasks if t.id == state.current_task_id), None) if not task: print("โš ๏ธ Current task not found, cannot create PR") return False # Create branch name from task ID branch_name = f"agent/{self.config.component}/{task.id}" branch_name = re.sub(r'[^a-zA-Z0-9/-]', '-', branch_name) # Get current branch name branch_result = subprocess.run( ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=self.config.project_root, capture_output=True, text=True ) current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "unknown" # If we're already on the target branch, just push it if current_branch == branch_name: print(f"โœ… Already on branch: {branch_name}") else: # Check if branch already exists locally check_branch = subprocess.run( ["git", "show-ref", "--verify", "--quiet", f"refs/heads/{branch_name}"], cwd=self.config.project_root, capture_output=True, text=True ) if check_branch.returncode == 0: # Branch exists locally, just checkout print(f"โš ๏ธ Branch {branch_name} already exists locally, checking out...") branch_result = subprocess.run( ["git", "checkout", branch_name], cwd=self.config.project_root, capture_output=True, text=True ) else: # Create and checkout new branch branch_result = subprocess.run( ["git", "checkout", "-b", branch_name], cwd=self.config.project_root, capture_output=True, text=True ) if branch_result.returncode != 0: print(f"โš ๏ธ Failed to create/checkout branch: {branch_result.stderr}") return False print(f"โœ… Created/checked out branch: {branch_name}") # Push branch to remote push_branch_result = subprocess.run( ["git", "push", "-u", "origin", branch_name], cwd=self.config.project_root, capture_output=True, text=True ) if push_branch_result.returncode != 0: print(f"โš ๏ธ Failed to push branch: {push_branch_result.stderr}") return False print(f"โœ… Pushed branch to remote") # Create PR if self._create_pull_request(task, branch_name): print("โœ… Successfully created PR") return True else: print("โš ๏ธ Failed to create PR, but branch was pushed") return False def _git_push(self) -> bool: """Push changes to remote. Behavior depends on commit_mode: - 'pr': Always create PR (don't try to push to master) - 'master': Try to push to master, create PR as fallback if push fails """ # Get current branch name branch_result = subprocess.run( ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=self.config.project_root, capture_output=True, text=True ) current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "unknown" # If commit_mode is 'pr', always create PR (don't try to push to master) if self.config.commit_mode == "pr": print("\n๐Ÿ“ Commit mode is 'pr', creating PR instead of pushing to master...") # Get current task state = self.task_manager.load_state() if not state.current_task_id: print("โš ๏ธ No current task ID, cannot create PR") return False tasks = self.task_manager.load_tasks() current_task = next((t for t in tasks if t.id == state.current_task_id), None) if not current_task: print("โš ๏ธ Current task not found, cannot create PR") return False # If we're on master, create branch and PR if current_branch == "master": return self._create_branch_and_pr(current_task) else: # We're already on a feature branch, just push it and create PR push_branch_result = subprocess.run( ["git", "push", "-u", "origin", current_branch], cwd=self.config.project_root, capture_output=True, text=True ) if push_branch_result.returncode == 0: print(f"โœ… Pushed branch to remote") if self._create_pull_request(current_task, current_branch): return True else: print("โš ๏ธ Failed to create PR, but branch was pushed") return True # Still consider it success else: print(f"โš ๏ธ Push failed: {push_branch_result.stderr}") return False # commit_mode is 'master' - try to push to master, fallback to PR if fails # First, fetch to check if there are remote changes fetch_result = subprocess.run( ["git", "fetch"], cwd=self.config.project_root, capture_output=True, text=True ) if fetch_result.returncode != 0: print(f"โš ๏ธ Failed to fetch: {fetch_result.stderr}") # Continue anyway, might be network issue # Check if local branch is behind remote check_result = subprocess.run( ["git", "rev-list", "--count", "HEAD..origin/master"], cwd=self.config.project_root, capture_output=True, text=True ) if check_result.returncode == 0: behind_count = check_result.stdout.strip() if behind_count and int(behind_count) > 0: print(f"โš ๏ธ Local branch is {behind_count} commit(s) behind remote") print(" Pulling latest changes before push...") if not self._git_pull(): print("โŒ Failed to pull, push may fail") # If we're on master, try to push directly if current_branch == "master": # Now try to push result = subprocess.run( ["git", "push"], cwd=self.config.project_root, capture_output=True, text=True ) if result.returncode == 0: print("โœ… Pushed to remote") return True else: # If push failed due to remote changes, try pull and push again if "fetch first" in result.stderr or "Updates were rejected" in result.stderr: print("โš ๏ธ Push rejected due to remote changes") print(" Pulling and retrying push...") if self._git_pull(): # Retry push after pull retry_result = subprocess.run( ["git", "push"], cwd=self.config.project_root, capture_output=True, text=True ) if retry_result.returncode == 0: print("โœ… Pushed to remote after pull") return True else: print(f"โŒ Push failed after pull: {retry_result.stderr}") # Fall through to create PR (fallback) else: print("โŒ Failed to pull before retry") # Fall through to create PR (fallback) # Fallback: create branch and PR print("\n๐Ÿ“ Push to master failed, creating branch and PR as fallback...") # Get current task state = self.task_manager.load_state() if not state.current_task_id: print("โš ๏ธ No current task ID, cannot create PR") return False tasks = self.task_manager.load_tasks() current_task = next((t for t in tasks if t.id == state.current_task_id), None) if not current_task: print("โš ๏ธ Current task not found, cannot create PR") return False return self._create_branch_and_pr(current_task) else: # We're already on a feature branch, just push it print(f"\n๐Ÿ“ Pushing to feature branch: {current_branch}") push_branch_result = subprocess.run( ["git", "push", "-u", "origin", current_branch], cwd=self.config.project_root, capture_output=True, text=True ) if push_branch_result.returncode == 0: print(f"โœ… Pushed branch to remote") # Try to create PR if we have task info state = self.task_manager.load_state() if state.current_task_id: tasks = self.task_manager.load_tasks() current_task = next((t for t in tasks if t.id == state.current_task_id), None) if current_task: self._create_pull_request(current_task, current_branch) return True else: print(f"โš ๏ธ Push failed: {push_branch_result.stderr}") return False def _create_issue_for_failed_task(self, task: Task) -> None: """Create GitHub/Forgejo issue for failed task.""" # TODO: Implement GitHub/Forgejo API integration print(f"\n๐Ÿ“‹ TODO: Create issue for failed task {task.id}") print(f" Title: Failed: {task.title}") print(f" Description: Task failed after {self.config.max_retries} retries") def main(): """Main entry point.""" if len(sys.argv) < 2: print("Usage: python agent_orchestrator.py ") print("Components: web_v2, backend, common") sys.exit(1) component = sys.argv[1] # Load configuration config = AgentConfig.from_env(component) try: config.validate() except ValueError as e: print(f"โŒ Configuration error: {e}") sys.exit(1) # Create orchestrator and run orchestrator = AgentOrchestrator(config) exit_code = orchestrator.run() sys.exit(exit_code) if __name__ == "__main__": main()