mnemo_cards/tools/agent/agent_orchestrator.py
2025-11-21 14:13:03 +03:00

994 lines
38 KiB
Python

"""
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 <component>")
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()