Files
roboco/roboco/agents/developer.py
T
2025-12-10 17:37:24 +01:00

623 lines
20 KiB
Python

"""
Developer Agent
Implementation of the Developer workflow from the blueprint.
Handles task lifecycle: SCAN → CLAIM → UNDERSTAND → PLAN → EXECUTE → VERIFY → NOTES → CLOSE
"""
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any
from uuid import UUID
import structlog
from roboco.agents.base import Agent, AgentConfig
from roboco.models import AgentRole, TaskStatus, Team
logger = structlog.get_logger()
class DevTaskPhase(str, Enum):
"""Phases of the developer task lifecycle."""
SCAN = "scan"
CLAIM = "claim"
UNDERSTAND = "understand"
PLAN = "plan"
EXECUTE = "execute"
VERIFY = "verify"
NOTES = "notes"
CLOSE = "close"
BLOCKED = "blocked"
@dataclass
class TaskContext:
"""Context for the current task being worked on."""
task_id: UUID
title: str
phase: DevTaskPhase = DevTaskPhase.CLAIM
subtasks: list[dict[str, Any]] = field(default_factory=list)
current_subtask: int = 0
blockers: list[str] = field(default_factory=list)
commits: list[str] = field(default_factory=list)
started_at: datetime = field(default_factory=datetime.utcnow)
journal_entries: list[str] = field(default_factory=list)
class DeveloperAgent(Agent):
"""
Developer agent that follows the Dev Lifecycle.
Workflow:
1. SCAN - Check for assigned/paused tasks
2. CLAIM - Lock and announce task
3. UNDERSTAND - Read requirements, ask if unclear
4. PLAN - Break into subtasks, create plan
5. EXECUTE - Work through subtasks, commit frequently
6. VERIFY - Self-test, run quality checks
7. NOTES - Document journey, create handoff
8. CLOSE - After QA approval, mark complete
"""
def __init__(self, config: AgentConfig) -> None:
"""Initialize developer agent."""
super().__init__(config)
self._task_context: TaskContext | None = None
self._cell_channel_id: UUID | None = None
@property
def cell_name(self) -> str:
"""Get the cell name based on team."""
if self.team == Team.BACKEND:
return "backend-cell"
elif self.team == Team.FRONTEND:
return "frontend-cell"
elif self.team == Team.UX_UI:
return "uxui-cell"
return "unknown-cell"
# =========================================================================
# LIFECYCLE IMPLEMENTATION
# =========================================================================
async def find_work(self) -> UUID | None:
"""
SCAN phase: Find available work.
Priority order:
1. Own paused/interrupted tasks
2. Assigned tasks
3. If none, signal availability to PM
"""
self.log.info("Scanning for work")
# Check for paused tasks first (highest priority)
paused_task = await self._find_paused_task()
if paused_task:
self.log.info("Found paused task", task_id=str(paused_task))
return paused_task
# Check for assigned tasks
assigned_task = await self._find_assigned_task()
if assigned_task:
self.log.info("Found assigned task", task_id=str(assigned_task))
return assigned_task
# Signal availability to PM
await self._signal_availability()
return None
async def execute_task(self, task_id: UUID) -> bool:
"""
Execute task through the developer lifecycle phases.
Returns True when task is completed (after QA + docs).
"""
# Initialize or restore task context
if self._task_context is None or self._task_context.task_id != task_id:
self._task_context = TaskContext(
task_id=task_id,
title=await self._get_task_title(task_id),
)
ctx = self._task_context
try:
match ctx.phase:
case DevTaskPhase.CLAIM:
await self._phase_claim(ctx)
ctx.phase = DevTaskPhase.UNDERSTAND
case DevTaskPhase.UNDERSTAND:
understood = await self._phase_understand(ctx)
if understood:
ctx.phase = DevTaskPhase.PLAN
# If not understood, stay in UNDERSTAND (asking questions)
case DevTaskPhase.PLAN:
await self._phase_plan(ctx)
ctx.phase = DevTaskPhase.EXECUTE
case DevTaskPhase.EXECUTE:
completed = await self._phase_execute(ctx)
if completed:
ctx.phase = DevTaskPhase.VERIFY
case DevTaskPhase.VERIFY:
verified = await self._phase_verify(ctx)
if verified:
ctx.phase = DevTaskPhase.NOTES
else:
ctx.phase = DevTaskPhase.EXECUTE # Back to fix issues
case DevTaskPhase.NOTES:
await self._phase_notes(ctx)
ctx.phase = DevTaskPhase.CLOSE
case DevTaskPhase.CLOSE:
closed = await self._phase_close(ctx)
if closed:
self._task_context = None
return True
case DevTaskPhase.BLOCKED:
resolved = await self._handle_blocked(ctx)
if resolved:
ctx.phase = DevTaskPhase.EXECUTE
return False
except Exception as e:
self.log.error("Error in task phase", phase=ctx.phase.value, error=str(e))
ctx.blockers.append(str(e))
ctx.phase = DevTaskPhase.BLOCKED
return False
# =========================================================================
# PHASE IMPLEMENTATIONS
# =========================================================================
async def _phase_claim(self, ctx: TaskContext) -> None:
"""
CLAIM phase: Lock the task and announce.
- Update task status to "claimed"
- Announce in cell channel
"""
self.log.info("CLAIM phase", task_id=str(ctx.task_id))
# Update task status
await self._update_task_status(ctx.task_id, TaskStatus.CLAIMED)
# Announce in channel
await self.send_message(
self._cell_channel_id or ctx.task_id, # Fallback if no channel
f"Claiming TASK-{str(ctx.task_id)[:8]}: {ctx.title}",
message_type="action",
)
# Journal entry
ctx.journal_entries.append(
f"[{datetime.utcnow().isoformat()}] Claimed task. Beginning work."
)
async def _phase_understand(self, ctx: TaskContext) -> bool:
"""
UNDERSTAND phase: Read and comprehend requirements.
- Read task record
- Read related code/docs
- Ask if unclear (GATE: must understand before proceeding)
Returns True if understood, False if still clarifying.
"""
self.log.info("UNDERSTAND phase", task_id=str(ctx.task_id))
# Read task requirements
requirements = await self._read_task_requirements(ctx.task_id)
# Use LLM to understand and identify gaps
prompt = f"""
You are analyzing a task before beginning work.
Task: {ctx.title}
Requirements:
{requirements}
Analyze:
1. What exactly needs to be done?
2. What are the acceptance criteria?
3. Is anything unclear that requires clarification?
If everything is clear, respond with: "UNDERSTOOD: [your understanding summary]"
If clarification needed, respond with: "QUESTION: [your question]"
"""
response = await self.think(prompt)
if response.startswith("UNDERSTOOD:"):
# Add understanding to journal
ctx.journal_entries.append(
f"[{datetime.utcnow().isoformat()}] Understanding: {response}"
)
return True
else:
# Ask question in channel
question = response.replace("QUESTION:", "").strip()
await self.send_message(
self._cell_channel_id or ctx.task_id,
f"Question about TASK-{str(ctx.task_id)[:8]}: {question}",
message_type="dialogue",
)
return False
async def _phase_plan(self, ctx: TaskContext) -> None:
"""
PLAN phase: Break task into subtasks.
- Create implementation plan
- Identify dependencies and risks
- Journal the approach
"""
self.log.info("PLAN phase", task_id=str(ctx.task_id))
# Use LLM to create plan
prompt = f"""
Create an implementation plan for this task:
Task: {ctx.title}
Understanding: {ctx.journal_entries[-1] if ctx.journal_entries else "No previous context"}
Break this into ordered subtasks. For each subtask:
- Clear description
- Files to modify
- Estimated complexity (small/medium/large)
Format as JSON array:
[
{{"description": "...", "files": ["..."], "complexity": "small|medium|large"}},
...
]
"""
response = await self.think(prompt)
# Parse subtasks (simplified - would use proper JSON parsing)
ctx.subtasks = [{"description": response, "files": [], "complexity": "medium"}]
# Journal entry
ctx.journal_entries.append(
f"[{datetime.utcnow().isoformat()}] Plan: {len(ctx.subtasks)} subtasks created"
)
# Announce plan
await self.send_message(
self._cell_channel_id or ctx.task_id,
f"TASK-{str(ctx.task_id)[:8]} plan ready: {len(ctx.subtasks)} subtasks",
message_type="action",
)
async def _phase_execute(self, ctx: TaskContext) -> bool:
"""
EXECUTE phase: Work through subtasks.
- Execute current subtask
- Commit with meaningful messages
- Update progress
Returns True when all subtasks complete.
"""
self.log.info(
"EXECUTE phase",
task_id=str(ctx.task_id),
subtask=ctx.current_subtask,
total=len(ctx.subtasks),
)
if ctx.current_subtask >= len(ctx.subtasks):
return True
subtask = ctx.subtasks[ctx.current_subtask]
# Use LLM to work on subtask
prompt = f"""
Execute this subtask:
Task: {ctx.title}
Subtask {ctx.current_subtask + 1}/{len(ctx.subtasks)}: {subtask.get("description", str(subtask))}
Provide:
1. Code changes needed
2. Commands to run
3. Commit message in format: type(scope): description
Respond with the implementation.
"""
response = await self.think_and_stream(prompt)
# Record work done
ctx.journal_entries.append(
f"[{datetime.utcnow().isoformat()}] Subtask {ctx.current_subtask + 1}: {response[:100]}..."
)
# Simulate commit (in real implementation would execute git)
commit_hash = f"commit_{ctx.current_subtask}"
ctx.commits.append(commit_hash)
# Progress update
await self.send_message(
self._cell_channel_id or ctx.task_id,
f"TASK-{str(ctx.task_id)[:8]} progress: subtask {ctx.current_subtask + 1}/{len(ctx.subtasks)} complete",
message_type="action",
)
ctx.current_subtask += 1
return ctx.current_subtask >= len(ctx.subtasks)
async def _phase_verify(self, ctx: TaskContext) -> bool:
"""
VERIFY phase: Self-test against acceptance criteria.
- Run quality checks (ruff, mypy, pytest)
- Self-review against acceptance criteria
- Flag for QA if passing
Returns True if verified, False if issues found.
"""
self.log.info("VERIFY phase", task_id=str(ctx.task_id))
# Run quality checks (simulated)
checks = [
("ruff format", True),
("ruff check", True),
("mypy", True),
("pytest", True),
]
all_passed = True
for check_name, passed in checks:
if not passed:
all_passed = False
ctx.journal_entries.append(
f"[{datetime.utcnow().isoformat()}] VERIFY FAILED: {check_name}"
)
if all_passed:
# Flag for QA
await self.send_message(
self._cell_channel_id or ctx.task_id,
f"TASK-{str(ctx.task_id)[:8]} ready for QA review. "
f"Commits: {', '.join(ctx.commits)}",
message_type="action",
)
ctx.journal_entries.append(
f"[{datetime.utcnow().isoformat()}] VERIFY PASSED. Flagged for QA."
)
return all_passed
async def _phase_notes(self, ctx: TaskContext) -> None:
"""
NOTES phase: Document journey and create handoff.
- Complete journey notes
- Link commits
- Create documenter handoff
"""
self.log.info("NOTES phase", task_id=str(ctx.task_id))
# Generate handoff using LLM
prompt = f"""
Create a documentation handoff for this completed task:
Task: {ctx.title}
Commits: {", ".join(ctx.commits)}
Journal:
{chr(10).join(ctx.journal_entries)}
Create a handoff summary including:
1. What was built
2. Key changes
3. Documentation needed
4. Code samples to include
"""
handoff = await self.think(prompt)
ctx.journal_entries.append(
f"[{datetime.utcnow().isoformat()}] Handoff created for documenter"
)
# Update task status
await self._update_task_status(ctx.task_id, TaskStatus.AWAITING_QA)
async def _phase_close(self, ctx: TaskContext) -> bool:
"""
CLOSE phase: After QA + documentation approval.
- Verify QA approved
- Verify documentation complete
- Mark task completed
Returns True if closed, False if waiting.
"""
self.log.info("CLOSE phase", task_id=str(ctx.task_id))
# Check if QA approved (simulated - would check actual status)
qa_approved = await self._check_qa_approved(ctx.task_id)
doc_complete = await self._check_docs_complete(ctx.task_id)
if qa_approved and doc_complete:
await self._update_task_status(ctx.task_id, TaskStatus.COMPLETED)
await self.send_message(
self._cell_channel_id or ctx.task_id,
f"TASK-{str(ctx.task_id)[:8]} completed!",
message_type="action",
)
return True
return False
async def _handle_blocked(self, ctx: TaskContext) -> bool:
"""
Handle blocked state.
- Document blocker
- Notify PM
- Wait for resolution
Returns True if resolved.
"""
self.log.info("BLOCKED", task_id=str(ctx.task_id), blockers=ctx.blockers)
if ctx.blockers:
blocker = ctx.blockers[-1]
await self.send_message(
self._cell_channel_id or ctx.task_id,
f"BLOCKED on TASK-{str(ctx.task_id)[:8]}: {blocker}",
message_type="blocker",
)
await self._update_task_status(ctx.task_id, TaskStatus.BLOCKED)
# Check if blocker resolved (simulated)
resolved = False
if resolved:
ctx.blockers.clear()
return resolved
# =========================================================================
# HELPER METHODS
# =========================================================================
async def _find_paused_task(self) -> UUID | None:
"""Find own paused/interrupted tasks."""
# TODO: Query task API for paused tasks assigned to this agent
return None
async def _find_assigned_task(self) -> UUID | None:
"""Find tasks assigned to this agent."""
# TODO: Query task API for assigned tasks
return None
async def _signal_availability(self) -> None:
"""Signal availability to PM."""
await self.send_message(
self._cell_channel_id or self.id,
f"{self.name} available for new tasks",
message_type="dialogue",
)
async def _get_task_title(self, task_id: UUID) -> str:
"""Get task title from API."""
# TODO: Query task API
return f"Task {str(task_id)[:8]}"
async def _read_task_requirements(self, task_id: UUID) -> str:
"""Read task requirements from task record."""
# TODO: Read from .tasks/active/TASK-XXX/
return "Requirements placeholder"
async def _update_task_status(self, task_id: UUID, status: TaskStatus) -> None:
"""Update task status via API."""
# TODO: Update via task API
self.log.info("Task status updated", task_id=str(task_id), status=status.value)
async def _check_qa_approved(self, task_id: UUID) -> bool:
"""Check if QA has approved the task."""
# TODO: Check via task API
return True # Simulated
async def _check_docs_complete(self, task_id: UUID) -> bool:
"""Check if documentation is complete."""
# TODO: Check via task API
return True # Simulated
def create_backend_developer(
name: str = "BE-Dev-1",
system_prompt: str | None = None,
) -> DeveloperAgent:
"""Factory function to create a backend developer agent."""
if system_prompt is None:
# Load from blueprint file
blueprint_path = Path("agents/blueprints/backend/be-dev.md")
if blueprint_path.exists():
content = blueprint_path.read_text()
# Extract system prompt section (between ```blocks after ## System Prompt)
import re
match = re.search(r"## System Prompt\s*```\s*(.*?)```", content, re.DOTALL)
system_prompt = match.group(1).strip() if match else ""
else:
system_prompt = "You are a backend developer."
config = AgentConfig(
name=name,
slug=name.lower().replace(" ", "-"),
role=AgentRole.DEVELOPER,
team=Team.BACKEND,
system_prompt=system_prompt,
capabilities=["code_execution", "git_operations", "file_management"],
)
return DeveloperAgent(config)
def create_frontend_developer(
name: str = "FE-Dev-1",
system_prompt: str | None = None,
) -> DeveloperAgent:
"""Factory function to create a frontend developer agent."""
if system_prompt is None:
blueprint_path = Path("agents/blueprints/frontend/fe-dev.md")
if blueprint_path.exists():
content = blueprint_path.read_text()
import re
match = re.search(r"## System Prompt\s*```\s*(.*?)```", content, re.DOTALL)
system_prompt = match.group(1).strip() if match else ""
else:
system_prompt = "You are a frontend developer."
config = AgentConfig(
name=name,
slug=name.lower().replace(" ", "-"),
role=AgentRole.DEVELOPER,
team=Team.FRONTEND,
system_prompt=system_prompt,
capabilities=["code_execution", "git_operations", "file_management"],
)
return DeveloperAgent(config)
def create_ux_developer(
name: str = "UX-Dev-1",
system_prompt: str | None = None,
) -> DeveloperAgent:
"""Factory function to create a UX/UI developer agent."""
if system_prompt is None:
blueprint_path = Path("agents/blueprints/ux_ui/ux-dev.md")
if blueprint_path.exists():
content = blueprint_path.read_text()
import re
match = re.search(r"## System Prompt\s*```\s*(.*?)```", content, re.DOTALL)
system_prompt = match.group(1).strip() if match else ""
else:
system_prompt = "You are a UX/UI developer."
config = AgentConfig(
name=name,
slug=name.lower().replace(" ", "-"),
role=AgentRole.DEVELOPER,
team=Team.UX_UI,
system_prompt=system_prompt,
capabilities=["design_tools", "file_management"],
)
return DeveloperAgent(config)