# Implementation Plan: TASK-008 - Resolve All TODOs ## Overview This plan breaks down the TODO resolution into discrete, testable sub-tasks organized by priority and dependency order. --- ## Phase 1: Critical Path (P0) - ~2.5 hours ### 1.1 Health Check Implementation (30 min) **File**: `roboco/api/routes/health.py` **Sub-tasks**: 1. Add database health check function 2. Add Redis health check function 3. Update `readiness_check()` endpoint to call both 4. Handle failures gracefully (return status per service) **Changes**: ```python # Add imports from sqlalchemy import text from roboco.db import get_async_session import redis.asyncio as redis from roboco.config import settings # Add check functions async def check_database() -> tuple[str, bool]: try: async with get_async_session() as session: await session.execute(text("SELECT 1")) return "ok", True except Exception as e: return str(e), False async def check_redis() -> tuple[str, bool]: try: client = redis.from_url(settings.redis_url) await client.ping() await client.close() return "ok", True except Exception as e: return str(e), False # Update endpoint @router.get("/ready") async def readiness_check() -> ReadinessResponse: db_status, db_ok = await check_database() redis_status, redis_ok = await check_redis() overall = "ok" if (db_ok and redis_ok) else "degraded" return ReadinessResponse( status=overall, database=db_status, redis=redis_status, ) ``` **Tests**: Call `/ready` with DB up/down, Redis up/down --- ### 1.2 Base Agent LLM Integration (2 hours) **File**: `roboco/agents/base.py` **Sub-tasks**: 1. Add Anthropic client initialization 2. Implement `think()` method with actual LLM call 3. Implement `think_and_stream()` with streaming response 4. Implement `send_message()` with HTTP API call 5. Implement `stream_reasoning()` with WebSocket broadcast **Changes**: ```python # Add to imports from anthropic import AsyncAnthropic import httpx from roboco.config import settings from roboco.api.websocket import broadcast_agent_chunk # Add to Agent.__init__ self._llm_client: AsyncAnthropic | None = None # Add property @property def llm_client(self) -> AsyncAnthropic: if self._llm_client is None: self._llm_client = AsyncAnthropic(api_key=settings.anthropic_api_key) return self._llm_client # Implement think() async def think(self, prompt: str, context: dict[str, Any] | None = None) -> str: self.log.debug("Thinking", prompt_length=len(prompt)) messages = [{"role": "user", "content": prompt}] response = await self.llm_client.messages.create( model=self.config.model, max_tokens=self.config.max_tokens, system=self.config.system_prompt, messages=messages, ) return response.content[0].text # Implement think_and_stream() async def think_and_stream( self, prompt: str, context: dict[str, Any] | None = None, ) -> str: self.log.debug("Thinking (streaming)", prompt_length=len(prompt)) messages = [{"role": "user", "content": prompt}] full_response = "" async with self.llm_client.messages.stream( model=self.config.model, max_tokens=self.config.max_tokens, system=self.config.system_prompt, messages=messages, ) as stream: async for text in stream.text_stream: full_response += text await self.stream_reasoning(text) return full_response # Implement send_message() async def send_message( self, channel_id: UUID, content: str, message_type: str = "dialogue", ) -> None: self.state.messages_sent += 1 self.state.last_activity = datetime.now(UTC) url = f"http://{settings.host}:{settings.port}/api/v1/messages" async with httpx.AsyncClient() as client: await client.post(url, json={ "channel_id": str(channel_id), "agent_id": str(self.id), "content": content, "message_type": message_type, }) self.log.debug("Message sent", channel_id=str(channel_id)) # Implement stream_reasoning() async def stream_reasoning(self, content: str) -> None: await broadcast_agent_chunk(self.id, content) self.log.debug("Streamed reasoning", content_length=len(content)) ``` **Tests**: Unit test with mocked Anthropic client --- ## Phase 2: Agent Functionality (P1) - ~5.5 hours ### 2.1 Agent API Helper (30 min) **File**: `roboco/agents/base.py` (add helper method) ```python async def _api_call( self, method: str, path: str, **kwargs: Any, ) -> dict[str, Any]: """Make API call to RoboCo services.""" url = f"http://{settings.host}:{settings.port}/api/v1{path}" async with httpx.AsyncClient() as client: response = await client.request(method, url, **kwargs) response.raise_for_status() return response.json() ``` --- ### 2.2 Developer Agent (1.5 hours) **File**: `roboco/agents/developer.py` **Sub-tasks**: 1. Implement `_find_paused_task()` - GET `/tasks?status=paused&assigned_to={id}` 2. Implement `_find_assigned_task()` - GET `/tasks?status=pending&assigned_to={id}` 3. Implement `_get_task_title()` - GET `/tasks/{id}` 4. Implement `_read_task_requirements()` - GET `/tasks/{id}` (description + criteria) 5. Implement `_update_task_status()` - PUT `/tasks/{id}` 6. Implement `_check_qa_approved()` - GET `/tasks/{id}` check status 7. Implement `_check_docs_complete()` - GET `/tasks/{id}/handoffs` **Pattern**: ```python async def _find_paused_task(self) -> UUID | None: result = await self._api_call( "GET", "/tasks", params={"status": "paused", "assigned_to": str(self.id)} ) tasks = result.get("items", []) return UUID(tasks[0]["id"]) if tasks else None ``` --- ### 2.3 QA Agent (1 hour) **File**: `roboco/agents/qa.py` **Sub-tasks**: 1. Implement `_find_awaiting_qa()` - GET `/tasks?status=awaiting_qa&team={team}` 2. Implement `_get_task_title()` - same as developer 3. Implement `_read_task_requirements()` - same as developer 4. Implement `_read_dev_notes()` - GET `/tasks/{id}` (dev_notes field) 5. Implement `_get_task_commits()` - GET `/tasks/{id}` (commits field) 6. Implement `_update_task_status()` - same as developer --- ### 2.4 Documenter Agent (1 hour) **File**: `roboco/agents/documenter.py` **Sub-tasks**: 1. Implement all query methods (same pattern as QA) 2. Implement `_phase_publish()` with file writing: ```python import aiofiles async def _phase_publish(self, ctx: DocContext) -> None: self.log.info("PUBLISH phase", task_id=str(ctx.task_id)) for doc_spec in ctx.documents_needed: if doc_spec.content: path = Path(doc_spec.path) path.parent.mkdir(parents=True, exist_ok=True) async with aiofiles.open(path, 'w') as f: await f.write(doc_spec.content) self.log.info("Published", path=doc_spec.path) await self._update_task_status(ctx.task_id, TaskStatus.COMPLETED) ``` --- ### 2.5 PM Agents (1 hour) **File**: `roboco/agents/pm.py` **Sub-tasks**: 1. Implement task counting methods using `/tasks` with filters 2. Implement agent counting using `/agents` endpoint 3. Implement `_get_pending_questions()` using `/messages?type=dialogue` 4. Implement `_check_task_progress()` using `/tasks/{id}` --- ### 2.6 Board Agents (1 hour) **File**: `roboco/agents/board.py` **Sub-tasks**: 1. Implement `_review_feature()` - GET task, check acceptance criteria 2. Implement `_read_channel_silently()` - GET `/channels/{slug}/messages` 3. Implement `_perform_audit()` - Aggregate queries for patterns --- ## Phase 3: Real-Time (P2) - ~2 hours ### 3.1 WebSocket Channel Validation (1 hour) **File**: `roboco/api/websocket.py` ```python from roboco.services.permissions import PermissionService async def validate_channel_access( channel_id: UUID, agent_id: UUID, session: AsyncSession ) -> bool: perm_service = PermissionService(session) return await perm_service.can_read_channel(agent_id, channel_id) ``` --- ### 3.2 Per-Agent Notification Delivery (1 hour) **File**: `roboco/api/websocket.py` Add agent-specific connections tracking: ```python class ConnectionManager: def __init__(self) -> None: # ... existing ... # Add per-agent notification connections self.notification_connections: dict[UUID, set[WebSocket]] = {} async def connect_notifications( self, websocket: WebSocket, agent_id: UUID ) -> None: await websocket.accept() if agent_id not in self.notification_connections: self.notification_connections[agent_id] = set() self.notification_connections[agent_id].add(websocket) async def broadcast_notification( agent_ids: list[UUID], notification_id: UUID, notification_type: str, subject: str, priority: str, ) -> None: event = { "type": "notification", "notification_id": str(notification_id), "notification_type": notification_type, "subject": subject, "priority": priority, "timestamp": datetime.now(UTC).isoformat(), } data = json.dumps(event) for agent_id in agent_ids: connections = manager.notification_connections.get(agent_id, set()) await asyncio.gather( *[conn.send_text(data) for conn in connections], return_exceptions=True, ) ``` --- ## Phase 4: Intelligence (P2) - ~3 hours ### 4.1 LLM-based Message Extraction (1 hour) **File**: `roboco/services/extraction.py` ```python async def extract_with_llm( self, content: str, agent_id: UUID, channel_id: UUID, session_id: UUID, group_id: UUID, task_id: UUID | None = None, ) -> ExtractionResult: from anthropic import AsyncAnthropic from roboco.config import settings client = AsyncAnthropic(api_key=settings.anthropic_api_key) # Use LLM to classify message segments prompt = f"""Classify the following agent output into message types. For each distinct segment, identify: - type: reasoning, dialogue, decision, action, blocker, or technical - content: the segment text - confidence: 0.0 to 1.0 Agent output: {content} Return as JSON array of objects with type, content, confidence.""" response = await client.messages.create( model="claude-3-haiku-20240307", # Fast, cheap for classification max_tokens=2000, messages=[{"role": "user", "content": prompt}], ) # Parse response and create ExtractedMessage objects # ... parsing logic ... return ExtractionResult(...) ``` --- ### 4.2 OptimalService RAG Methods (2 hours) **File**: `roboco/services/optimal.py` Implement `search()`, `query()`, `index()` using piragi: ```python async def search( self, query: str, index_types: list[IndexType] | None = None, limit: int = 10, ) -> list[SearchResult]: """Search across knowledge base indexes.""" types_to_search = index_types or list(IndexType) all_results: list[SearchResult] = [] for index_type in types_to_search: index = self._get_index(index_type) # Use piragi's search method results = await index.search(query, top_k=limit) for r in results: all_results.append(SearchResult( content=r.content, source=r.metadata.get("source", "unknown"), score=r.score, index_type=index_type, metadata=r.metadata, )) # Sort by score, return top limit all_results.sort(key=lambda x: x.score, reverse=True) return all_results[:limit] async def query( self, query: str, context: QueryContext | None = None, ) -> RAGResponse: """RAG query with context.""" # Get relevant context results = await self.search( query, index_types=context.index_types if context else None, limit=5, ) # Build context for LLM context_text = "\n\n".join([r.content for r in results]) # Query with RAG context prompt = f"""Based on the following context, answer the question. Context: {context_text} Question: {query} Answer:""" from anthropic import AsyncAnthropic client = AsyncAnthropic(api_key=settings.anthropic_api_key) response = await client.messages.create( model=settings.default_model, max_tokens=1000, messages=[{"role": "user", "content": prompt}], ) return RAGResponse( answer=response.content[0].text, citations=results, query=query, context_used=len(results), ) ``` --- ## Validation Checklist After each phase, verify: - [ ] `uv run ruff format .` passes - [ ] `uv run ruff check .` passes - [ ] `uv run mypy roboco/` passes - [ ] No TODO comments remain in modified files - [ ] API calls work against running server --- ## Rollback Plan Each phase is independent. If issues arise: 1. Revert the specific file changes 2. Leave TODO comments in place for that section 3. Create a new sub-task for the problematic area --- ## Definition of Done - [ ] All 37 TODOs resolved or converted to tracked issues - [ ] Health checks verify actual service connectivity - [ ] Agents can call LLM and receive responses - [ ] Agents can send messages via API - [ ] WebSocket notifications delivered to connected agents - [ ] RAG queries return relevant results - [ ] All type checks pass - [ ] All linting passes