Files
timmy-config/wizards/allegro-primus/git_tools/task_runner.py
2026-03-31 20:02:01 +00:00

425 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Task Runner for Allegro-Primus
Executes tasks in isolated worktrees with result capture
"""
import json
import os
import subprocess
import sys
import time
import traceback
from dataclasses import dataclass, asdict
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Callable, Any
from contextlib import contextmanager
@dataclass
class TaskResult:
"""Result of a task execution"""
task_id: str
issue_number: str
success: bool
return_code: int
stdout: str
stderr: str
execution_time: float
timestamp: str
artifacts: List[str]
commit_hash: Optional[str] = None
error: Optional[str] = None
def to_dict(self) -> Dict:
return asdict(self)
def to_json(self, indent: int = 2) -> str:
return json.dumps(self.to_dict(), indent=indent)
class TaskRunner:
"""Runs tasks in isolated worktree environments"""
def __init__(self, worktree_path: str, logs_dir: str = None):
self.worktree_path = Path(worktree_path)
self.logs_dir = Path(logs_dir) if logs_dir else self.worktree_path / ".ap-logs"
self.logs_dir.mkdir(exist_ok=True)
self.task_history: List[TaskResult] = []
def _run_git(self, args: List[str], cwd: str = None) -> subprocess.CompletedProcess:
"""Run git command in worktree"""
return subprocess.run(
["git"] + args,
capture_output=True,
text=True,
cwd=cwd or str(self.worktree_path)
)
def _write_log(self, task_id: str, content: str):
"""Write content to task log file"""
log_file = self.logs_dir / f"{task_id}.log"
log_file.write_text(content)
@contextmanager
def _capture_context(self):
"""Context manager for capturing output"""
yield
def run_command(self,
task_id: str,
command: List[str],
issue_number: str,
env: Dict[str, str] = None,
timeout: int = 300,
capture_output: bool = True) -> TaskResult:
"""
Run a shell command in the worktree
Args:
task_id: Unique identifier for this task
command: Command and arguments as list
issue_number: Associated issue number
env: Additional environment variables
timeout: Command timeout in seconds
capture_output: Whether to capture stdout/stderr
Returns:
TaskResult with execution details
"""
start_time = time.time()
timestamp = datetime.now().isoformat()
# Prepare environment
run_env = os.environ.copy()
run_env["AP_WORKTREE"] = str(self.worktree_path)
run_env["AP_ISSUE"] = issue_number
run_env["AP_TASK_ID"] = task_id
if env:
run_env.update(env)
try:
# Run command
result = subprocess.run(
command,
capture_output=capture_output,
text=True,
cwd=str(self.worktree_path),
env=run_env,
timeout=timeout
)
execution_time = time.time() - start_time
# Create result
task_result = TaskResult(
task_id=task_id,
issue_number=issue_number,
success=result.returncode == 0,
return_code=result.returncode,
stdout=result.stdout,
stderr=result.stderr,
execution_time=execution_time,
timestamp=timestamp,
artifacts=[]
)
# Write log
log_content = f"""Task: {task_id}
Issue: {issue_number}
Command: {' '.join(command)}
Timestamp: {timestamp}
Duration: {execution_time:.2f}s
Return Code: {result.returncode}
=== STDOUT ===
{result.stdout}
=== STDERR ===
{result.stderr}
"""
self._write_log(task_id, log_content)
except subprocess.TimeoutExpired as e:
execution_time = time.time() - start_time
task_result = TaskResult(
task_id=task_id,
issue_number=issue_number,
success=False,
return_code=-1,
stdout=e.stdout if e.stdout else "",
stderr=f"Timeout after {timeout}s",
execution_time=execution_time,
timestamp=timestamp,
artifacts=[],
error="Timeout"
)
except Exception as e:
execution_time = time.time() - start_time
task_result = TaskResult(
task_id=task_id,
issue_number=issue_number,
success=False,
return_code=-1,
stdout="",
stderr=str(e),
execution_time=execution_time,
timestamp=timestamp,
artifacts=[],
error=traceback.format_exc()
)
self.task_history.append(task_result)
return task_result
def run_script(self,
task_id: str,
script_content: str,
issue_number: str,
interpreter: str = "python3",
timeout: int = 300) -> TaskResult:
"""
Run a script in the worktree
Args:
task_id: Task identifier
script_content: Script to execute
issue_number: Associated issue
interpreter: Script interpreter
timeout: Execution timeout
"""
# Write script to temporary file
script_path = self.worktree_path / f".ap-script-{task_id}.py"
script_path.write_text(script_content)
try:
result = self.run_command(
task_id=task_id,
command=[interpreter, str(script_path)],
issue_number=issue_number,
timeout=timeout
)
finally:
# Cleanup script
script_path.unlink(missing_ok=True)
return result
def run_python_function(self,
task_id: str,
func: Callable,
issue_number: str,
args: tuple = (),
kwargs: Dict = None) -> TaskResult:
"""
Execute a Python function in the worktree context
Args:
task_id: Task identifier
func: Function to execute
issue_number: Associated issue
args: Positional arguments
kwargs: Keyword arguments
"""
start_time = time.time()
timestamp = datetime.now().isoformat()
kwargs = kwargs or {}
# Store original cwd
original_cwd = os.getcwd()
try:
# Change to worktree
os.chdir(self.worktree_path)
# Set environment markers
os.environ["AP_WORKTREE"] = str(self.worktree_path)
os.environ["AP_ISSUE"] = issue_number
os.environ["AP_TASK_ID"] = task_id
# Execute function
result = func(*args, **kwargs)
execution_time = time.time() - start_time
task_result = TaskResult(
task_id=task_id,
issue_number=issue_number,
success=True,
return_code=0,
stdout=str(result) if result else "",
stderr="",
execution_time=execution_time,
timestamp=timestamp,
artifacts=[]
)
except Exception as e:
execution_time = time.time() - start_time
task_result = TaskResult(
task_id=task_id,
issue_number=issue_number,
success=False,
return_code=-1,
stdout="",
stderr=str(e),
execution_time=execution_time,
timestamp=timestamp,
artifacts=[],
error=traceback.format_exc()
)
finally:
os.chdir(original_cwd)
self.task_history.append(task_result)
return task_result
def auto_commit(self,
message: str,
files: List[str] = None,
allow_empty: bool = False) -> Optional[str]:
"""
Automatically commit changes in the worktree
Args:
message: Commit message
files: Specific files to commit (None = all changes)
allow_empty: Allow empty commits
Returns:
Commit hash if successful
"""
# Add files
if files:
for f in files:
self._run_git(["add", f])
else:
self._run_git(["add", "-A"])
# Check if there are changes
status = self._run_git(["status", "--porcelain"])
if not status.stdout.strip() and not allow_empty:
return None
# Commit
cmd = ["commit", "-m", f"[AP] {message}"]
if allow_empty:
cmd.append("--allow-empty")
result = self._run_git(cmd)
if result.returncode == 0:
# Get commit hash
log_result = self._run_git(["log", "-1", "--format=%H"])
return log_result.stdout.strip()
return None
def get_logs(self, task_id: str = None) -> str:
"""Get logs for a task or all tasks"""
if task_id:
log_file = self.logs_dir / f"{task_id}.log"
if log_file.exists():
return log_file.read_text()
return ""
# Return all logs
all_logs = []
for log_file in sorted(self.logs_dir.glob("*.log")):
all_logs.append(f"=== {log_file.stem} ===")
all_logs.append(log_file.read_text())
all_logs.append("")
return "\n".join(all_logs)
def generate_report(self, output_path: str = None) -> str:
"""Generate a report of all task executions"""
report = {
"worktree": str(self.worktree_path),
"total_tasks": len(self.task_history),
"successful": sum(1 for t in self.task_history if t.success),
"failed": sum(1 for t in self.task_history if not t.success),
"tasks": [t.to_dict() for t in self.task_history]
}
report_json = json.dumps(report, indent=2)
if output_path:
Path(output_path).write_text(report_json)
return report_json
class TaskBatch:
"""Batch multiple tasks for sequential execution"""
def __init__(self, runner: TaskRunner, issue_number: str):
self.runner = runner
self.issue_number = issue_number
self.tasks: List[Dict] = []
self.results: List[TaskResult] = []
def add_command(self, task_id: str, command: List[str], **kwargs):
"""Add a command task to the batch"""
self.tasks.append({
"type": "command",
"task_id": task_id,
"command": command,
"kwargs": kwargs
})
def add_script(self, task_id: str, script: str, **kwargs):
"""Add a script task to the batch"""
self.tasks.append({
"type": "script",
"task_id": task_id,
"script": script,
"kwargs": kwargs
})
def execute(self, stop_on_error: bool = True) -> List[TaskResult]:
"""Execute all tasks in the batch"""
self.results = []
for task in self.tasks:
if task["type"] == "command":
result = self.runner.run_command(
task_id=task["task_id"],
command=task["command"],
issue_number=self.issue_number,
**task["kwargs"]
)
elif task["type"] == "script":
result = self.runner.run_script(
task_id=task["task_id"],
script_content=task["script"],
issue_number=self.issue_number,
**task["kwargs"]
)
self.results.append(result)
if stop_on_error and not result.success:
break
return self.results
if __name__ == "__main__":
# Test
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
runner = TaskRunner(tmpdir)
# Test command execution
result = runner.run_command(
task_id="test-1",
command=["echo", "Hello from AP"],
issue_number="123"
)
print(f"Success: {result.success}")
print(f"Output: {result.stdout}")