Documenter is ignored but it works pretty well

This commit is contained in:
Renn F
2025-12-25 07:40:39 +01:00
parent 15f5be3f8b
commit 7f13e10bf4
13 changed files with 386 additions and 99 deletions
+2 -7
View File
@@ -1,13 +1,9 @@
services:
# ==========================================================================
# PostgreSQL - Primary Database with pgvector for RAG
# Uses Docker Hardened Image (DHI) base with pgvector extension
# ==========================================================================
postgres:
build:
context: .
dockerfile: docker/postgres-pgvector.Dockerfile
image: roboco-postgres-pgvector
image: pgvector/pgvector:pg16
container_name: roboco-postgres
restart: unless-stopped
environment:
@@ -26,10 +22,9 @@ services:
# ==========================================================================
# Redis - Cache, Sessions, Event Bus
# Uses Docker Hardened Image (DHI) for enhanced security
# ==========================================================================
redis:
image: dhi.io/redis:8-alpine
image: redis:8-alpine
container_name: roboco-redis
restart: unless-stopped
command: redis-server --appendonly yes
+3 -3
View File
@@ -1,10 +1,10 @@
# =============================================================================
# Agent Base Image - Docker Hardened Image (DHI)
# Agent Base Image
# =============================================================================
# Uses DHI Python 3.13 with dev tools for Claude Code agent containers
# Python 3.13 with dev tools for Claude Code agent containers
# =============================================================================
FROM dhi.io/python:3.13-debian13-dev
FROM python:3.13-bookworm
# Install Node.js 22 (required for Claude Code CLI)
RUN apt-get update && apt-get install -y --no-install-recommends \
+3 -3
View File
@@ -1,10 +1,10 @@
# =============================================================================
# Orchestrator - Docker Hardened Image (DHI)
# Orchestrator
# =============================================================================
# API Server + Agent Spawner using DHI Python 3.13
# API Server + Agent Spawner using Python 3.13
# =============================================================================
FROM dhi.io/python:3.13-debian13-dev
FROM python:3.13-bookworm
# Install dependencies + Docker CLI
RUN apt-get update && apt-get install -y --no-install-recommends \
+6 -36
View File
@@ -1,44 +1,14 @@
# =============================================================================
# PostgreSQL with pgvector on Docker Hardened Image
# PostgreSQL with pgvector (Custom Build Example)
# =============================================================================
# Multi-stage build:
# 1. Build pgvector extension using standard postgres image (has build tools)
# 2. Copy compiled extension to DHI postgres (minimal, secure runtime)
# Example: Build pgvector from source on standard postgres image
# Currently unused - docker-compose.yml uses pgvector/pgvector:pg16 directly
# =============================================================================
# -----------------------------------------------------------------------------
# Stage 1: Build pgvector extension
# -----------------------------------------------------------------------------
FROM postgres:17 AS builder
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
git \
postgresql-server-dev-17 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Clone and build pgvector (use tagged release for reproducibility)
ARG PGVECTOR_VERSION=0.8.1
RUN git clone --branch v${PGVECTOR_VERSION} --depth 1 https://github.com/pgvector/pgvector.git /tmp/pgvector \
&& cd /tmp/pgvector \
&& make OPTFLAGS="" \
&& make install
# -----------------------------------------------------------------------------
# Stage 2: DHI Runtime with pgvector
# -----------------------------------------------------------------------------
FROM dhi.io/postgres:17-debian13
# Copy pgvector extension files from builder
# Extension shared library
COPY --from=builder /usr/lib/postgresql/17/lib/vector.so /usr/lib/postgresql/17/lib/
# Extension control and SQL files
COPY --from=builder /usr/share/postgresql/17/extension/vector* /usr/share/postgresql/17/extension/
FROM pgvector/pgvector:pg17
# Add init script to create extension on startup
COPY docker/postgres-init/01-create-extensions.sql /docker-entrypoint-initdb.d/
LABEL org.opencontainers.image.title="PostgreSQL with pgvector (DHI)"
LABEL org.opencontainers.image.description="Docker Hardened PostgreSQL 17 with pgvector extension for vector similarity search"
LABEL org.opencontainers.image.title="PostgreSQL with pgvector"
LABEL org.opencontainers.image.description="PostgreSQL 17 with pgvector extension for vector similarity search"
+10 -2
View File
@@ -291,9 +291,17 @@ Respond with structured analysis.
# PLAN: Save documentation plan to task API (required before start)
plan_data = {
"approach": f"Document {ctx.title}",
"steps": [doc.title for doc in ctx.documents_needed],
"sub_tasks": [
{
"id": f"doc-{i}",
"title": doc.title,
"description": f"Write {doc.doc_type.value} at {doc.path}",
"completed": False,
"order": i,
}
for i, doc in enumerate(ctx.documents_needed)
],
"risks": [],
"estimated_sessions": 1,
}
await self._api_call("PATCH", f"/tasks/{ctx.task_id}", json={"plan": plan_data})
+24 -4
View File
@@ -23,11 +23,31 @@ _DEFAULT_PROMPTS = {
Team.UX_UI: "You are a UX/UI developer.",
}
# Default capabilities for each team
# Default capabilities for each team (matches blueprint capabilities)
_CAPABILITIES = {
Team.BACKEND: ["code_execution", "git_operations", "file_management"],
Team.FRONTEND: ["code_execution", "git_operations", "file_management"],
Team.UX_UI: ["design_tools", "file_management"],
Team.BACKEND: [
"code_execution",
"git_operations",
"file_management",
"api_development",
"database_design",
],
Team.FRONTEND: [
"code_execution",
"git_operations",
"file_management",
"browser_testing",
"accessibility_testing",
"responsive_design",
],
Team.UX_UI: [
"design_tools",
"file_management",
"figma_expertise",
"prototyping",
"design_system_management",
"accessibility_design",
],
}
+20 -4
View File
@@ -23,11 +23,27 @@ _DEFAULT_PROMPTS = {
Team.UX_UI: "You are a UX/UI documenter.",
}
# Default capabilities for each team
# Default capabilities for each team (matches blueprint capabilities)
_CAPABILITIES = {
Team.BACKEND: ["documentation", "file_management"],
Team.FRONTEND: ["documentation", "storybook", "file_management"],
Team.UX_UI: ["documentation", "design_system_docs"],
Team.BACKEND: [
"technical_writing",
"api_documentation",
"code_reading",
"file_management",
],
Team.FRONTEND: [
"technical_writing",
"component_documentation",
"code_reading",
"storybook",
"file_management",
],
Team.UX_UI: [
"design_documentation",
"design_system_maintenance",
"technical_writing",
"file_management",
],
}
+18 -4
View File
@@ -23,11 +23,25 @@ _DEFAULT_PROMPTS = {
Team.UX_UI: "You are a UX/UI QA engineer.",
}
# Default capabilities for each team
# Default capabilities for each team (matches blueprint capabilities)
_CAPABILITIES = {
Team.BACKEND: ["code_review", "test_execution", "security_analysis"],
Team.FRONTEND: ["visual_testing", "a11y_testing", "browser_testing"],
Team.UX_UI: ["design_review", "consistency_check", "a11y_testing"],
Team.BACKEND: [
"code_review",
"test_execution",
"security_analysis",
"quality_assurance",
],
Team.FRONTEND: [
"visual_testing",
"accessibility_testing",
"browser_testing",
"quality_assurance",
],
Team.UX_UI: [
"design_review",
"accessibility_review",
"quality_assurance",
],
}
+210 -22
View File
@@ -311,34 +311,121 @@ class CellPMAgent(Agent, CyclicPhaseRunner[CellPMPhase]):
}
async def _delegate_task(self, task_id: UUID, task: dict[str, Any]) -> bool:
"""Delegate task by creating subtasks for developers."""
# Simple delegation - create one subtask assigned to available dev
"""Delegate task by creating subtasks for developers.
Follows blueprint workflow:
CREATE (backlog) → SESSION → ACTIVATE (pending) → NOTIFY
"""
# Find available developer
best_dev = await self._find_best_dev(task_id)
if not best_dev:
self.log.warning("No available developer for task", task_id=str(task_id))
return False
# Create subtask
await self._api_call(
team = self.team.value if self.team else "backend"
# Step 1: Create subtask with status "backlog" (prevents premature pickup)
subtask_resp = await self._api_call(
"POST",
"/tasks",
json={
"title": f"Implement: {task.get('title', 'Task')}",
"description": task.get("description", ""),
"team": self.team.value if self.team else "backend",
"team": team,
"acceptance_criteria": task.get("acceptance_criteria", []),
"parent_task_id": str(task_id),
"assigned_to": str(best_dev.agent_id),
"status": "pending",
"status": "backlog", # Backlog until session is ready
},
)
subtask_id = subtask_resp.get("id")
if not subtask_id:
self.log.error("Failed to create subtask", parent_task_id=str(task_id))
return False
self.log.info(
"PM created subtask",
"PM created subtask (backlog)",
subtask_id=subtask_id,
parent_task_id=str(task_id),
assigned_to=str(best_dev.agent_id),
)
# Step 2: Create session for the subtask
channel_slug = self._get_team_channel(team)
try:
await self._api_call(
"POST",
"/sessions/for-tasks",
json={
"task_ids": [subtask_id],
"channel_slug": channel_slug,
"scope": f"Work session for {task.get('title', 'task')}",
"relationship_type": "implements",
},
)
self.log.info("Session created for subtask", subtask_id=subtask_id)
except Exception as e:
self.log.warning(
"Session creation failed, activating anyway",
subtask_id=subtask_id,
error=str(e),
)
# Step 3: Activate subtask (changes status to pending)
try:
await self._api_call("POST", f"/tasks/{subtask_id}/activate")
self.log.info("Subtask activated", subtask_id=subtask_id)
except Exception as e:
self.log.error(
"Failed to activate subtask",
subtask_id=subtask_id,
error=str(e),
)
return False
# Step 4: Notify assigned developer
try:
await self._notify_developer(best_dev, subtask_id, task)
except Exception as e:
self.log.warning(
"Failed to notify developer (task still assigned)",
error=str(e),
)
return True
def _get_team_channel(self, team: str) -> str:
"""Get the channel slug for a team."""
channel_map = {
"backend": "backend-cell",
"frontend": "frontend-cell",
"ux_ui": "uxui-cell",
}
return channel_map.get(team, "backend-cell")
async def _notify_developer(
self, dev: Any, subtask_id: str, task: dict[str, Any]
) -> None:
"""Notify developer of new task assignment."""
await self._api_call(
"POST",
"/notifications",
json={
"type": "task_assigned",
"priority": "normal",
"to_agents": [str(dev.agent_id)],
"subject": f"New task: {task.get('title', 'Task')}",
"body": (
f"You've been assigned a new task.\n\n"
f"Task ID: {subtask_id}\n"
f"Title: {task.get('title', 'Task')}\n\n"
f"Use roboco_task_scan to find and claim it."
),
"related_task_id": subtask_id,
"requires_ack": False,
},
)
# =========================================================================
# CELL PM PHASES
# =========================================================================
@@ -445,11 +532,15 @@ medium,TASK-abc123,P1,backend-dev-1
# Check for pending questions in channel
questions = await self._get_pending_questions()
for question in questions:
for question_data in questions:
question_content = question_data.get("content", "")
task_id = question_data.get("task_id")
session_id = question_data.get("session_id")
# Use TOON for token-efficient context encoding
question_context = self.format_context_labeled(
"Cell Question",
{"question": question, "cell": self.cell_name},
{"question": question_content, "cell": self.cell_name},
)
prompt = f"""A cell member needs help:
@@ -464,12 +555,26 @@ As the Cell PM, provide:
Be helpful and unblock the team.
"""
response = await self.think(prompt)
# TODO: Questions should include task_id for routing
# For now, log that we can't route without session context
self.log.info(
"PM response (no session context - need task_id in questions)",
response_preview=response[:100],
)
# Send response with task_id for proper routing
if session_id:
await self.send_message(
UUID(session_id),
response,
message_type="answer",
task_id=UUID(task_id) if task_id else None,
)
self.log.info(
"PM responded to question",
task_id=task_id,
response_preview=response[:100],
)
else:
self.log.info(
"PM response (no session - using channel)",
task_id=task_id,
response_preview=response[:100],
)
async def _phase_escalate(self) -> None:
"""
@@ -626,15 +731,26 @@ Be helpful and unblock the team.
except Exception as e:
self.log.error("Failed to assign task", error=str(e))
async def _get_pending_questions(self) -> list[str]:
"""Get unanswered questions from channel."""
async def _get_pending_questions(self) -> list[dict[str, Any]]:
"""Get unanswered questions from channel.
Returns full message info including task_id for routing.
"""
try:
result = await self._api_call(
"GET",
"/messages",
params={"message_type": "dialogue", "unanswered": True},
)
return [m.get("content", "") for m in result.get("items", [])]
return [
{
"content": m.get("content", ""),
"task_id": m.get("task_id"),
"session_id": m.get("session_id"),
"from_agent": m.get("from_agent"),
}
for m in result.get("items", [])
]
except Exception as e:
self.log.warning("Failed to get pending questions", error=str(e))
return []
@@ -1073,8 +1189,13 @@ class MainPMAgent(Agent, CyclicPhaseRunner[MainPMPhase]):
async def _create_cell_task(
self, parent_id: UUID, task: dict[str, Any], team: str, pm_slug: str
) -> None:
"""Create a task for a Cell PM."""
await self._api_call(
"""Create a task for a Cell PM.
Follows blueprint workflow:
CREATE (backlog) → GROUP → SESSION → ACTIVATE (pending) → NOTIFY
"""
# Step 1: Create task with status "backlog"
task_resp = await self._api_call(
"POST",
"/tasks",
json={
@@ -1084,16 +1205,83 @@ class MainPMAgent(Agent, CyclicPhaseRunner[MainPMPhase]):
"acceptance_criteria": task.get("acceptance_criteria", []),
"parent_task_id": str(parent_id),
"assigned_to": pm_slug,
"status": "pending",
"status": "backlog", # Backlog until session is ready
},
)
cell_task_id = task_resp.get("id")
if not cell_task_id:
self.log.error("Failed to create cell task", parent_id=str(parent_id))
return
self.log.info(
"Main PM created cell task",
"Main PM created cell task (backlog)",
cell_task_id=cell_task_id,
parent_task_id=str(parent_id),
team=team,
assigned_to=pm_slug,
)
# Step 2: Create group if needed (cross-cell initiatives)
channel_slug = self._get_team_channel(team)
# Step 3: Create session for the cell task
try:
await self._api_call(
"POST",
"/sessions/for-tasks",
json={
"task_ids": [cell_task_id],
"channel_slug": channel_slug,
"scope": f"Cell work for {task.get('title', 'initiative')}",
"relationship_type": "implements",
},
)
self.log.info("Session created for cell task", cell_task_id=cell_task_id)
except Exception as e:
self.log.warning(
"Session creation failed, activating anyway",
cell_task_id=cell_task_id,
error=str(e),
)
# Step 4: Activate task (changes status to pending)
try:
await self._api_call("POST", f"/tasks/{cell_task_id}/activate")
self.log.info("Cell task activated", cell_task_id=cell_task_id)
except Exception as e:
self.log.error(
"Failed to activate cell task",
cell_task_id=cell_task_id,
error=str(e),
)
return
# Step 5: Notify Cell PM
try:
await self._api_call(
"POST",
"/notifications",
json={
"type": "task_assigned",
"priority": "normal",
"to_agents": [pm_slug],
"subject": f"New initiative: {task.get('title', 'Task')}",
"body": (
f"A new initiative has been assigned to your cell.\n\n"
f"Task ID: {cell_task_id}\n"
f"Title: {task.get('title', 'Task')}\n\n"
f"Please triage and delegate to your team."
),
"related_task_id": cell_task_id,
"requires_ack": True,
},
)
except Exception as e:
self.log.warning(
"Failed to notify Cell PM (task still assigned)",
error=str(e),
)
# =========================================================================
# MAIN PM PHASES
# =========================================================================
+10 -2
View File
@@ -271,9 +271,17 @@ Acceptance Criteria,Verify all criteria met,Review implementation|Check each cri
# PLAN: Save test plan to task API (required before start)
plan_data = {
"approach": f"QA review of {ctx.title}",
"steps": [tc.name for tc in ctx.test_cases],
"sub_tasks": [
{
"id": f"test-{i}",
"title": tc.name,
"description": tc.description,
"completed": False,
"order": i,
}
for i, tc in enumerate(ctx.test_cases)
],
"risks": [],
"estimated_sessions": 1,
}
await self._api_call("PATCH", f"/tasks/{ctx.task_id}", json={"plan": plan_data})
+7
View File
@@ -15,6 +15,7 @@ from roboco.api.middleware import setup_middleware
from roboco.api.routes.agents import router as agents_router
from roboco.api.routes.channels import router as channels_router
from roboco.api.routes.dashboard import router as dashboard_router
from roboco.api.routes.groups import router as groups_router
from roboco.api.routes.health import router as health_router
from roboco.api.routes.journals import router as journals_router
from roboco.api.routes.kanban import router as kanban_router
@@ -159,6 +160,12 @@ def create_app() -> FastAPI:
tags=["Channels"],
)
app.include_router(
groups_router,
prefix=f"{api_prefix}/groups",
tags=["Groups"],
)
app.include_router(
sessions_router,
prefix=f"{api_prefix}/sessions",
+43 -8
View File
@@ -240,24 +240,59 @@ def _check_recipients(agent_id: str, recipients: list[str]) -> dict[str, Any] |
return None
def _validate_send_input(
agent_id: str, data: SendNotificationInput
) -> dict[str, Any] | None:
"""Validate all send notification inputs. Returns error or None."""
for check in [
lambda: _check_send_permission(agent_id),
lambda: _check_recipients(agent_id, data.recipients),
lambda: _validate_notification_type(data.notification_type),
lambda: _validate_priority(data.priority),
]:
if error := check():
return error
return None
async def _resolve_recipients(
recipients: list[str], client: "ApiClient"
) -> tuple[list[str], dict[str, Any] | None]:
"""Resolve recipient slugs to UUIDs. Returns (resolved_list, error_or_none)."""
from roboco.mcp.utils import resolve_agent_uuid_cached
resolved: list[str] = []
unresolved: list[str] = []
for recipient in recipients:
uuid = await resolve_agent_uuid_cached(recipient, client)
if uuid:
resolved.append(uuid)
else:
unresolved.append(recipient)
if unresolved:
return [], format_error_response(
"RECIPIENT_NOT_FOUND",
f"Could not resolve recipient(s): {', '.join(unresolved)}",
)
return resolved, None
async def _handle_send(
client: ApiClient, agent_id: str, data: SendNotificationInput
) -> dict[str, Any]:
"""Handle sending a notification."""
# Validate permissions and data
if error := _check_send_permission(agent_id):
if error := _validate_send_input(agent_id, data):
return error
if error := _check_recipients(agent_id, data.recipients):
return error
if error := _validate_notification_type(data.notification_type):
return error
if error := _validate_priority(data.priority):
resolved_recipients, error = await _resolve_recipients(data.recipients, client)
if error:
return error
payload = {
"type": data.notification_type,
"priority": data.priority,
"to_agents": data.recipients,
"to_agents": resolved_recipients,
"subject": data.subject,
"body": data.body,
"requires_ack": data.requires_ack,
+30 -4
View File
@@ -1418,16 +1418,42 @@ Start now: roboco_task_get("{task_id}")
This is the FIRST dispatcher called - it classifies unassigned tasks
and routes them to Board, Main PM, Cell PM, or directly to devs.
Also handles already-assigned pending tasks for PM agents.
Monitors: pending tasks with no assigned_to
Monitors: pending tasks (both assigned and unassigned)
Spawns: product-owner, main-pm, be-pm, fe-pm, ux-pm (or devs for simple)
"""
# Get pending tasks that haven't been assigned yet
# Get pending tasks
tasks = await self._fetch_tasks(client, "pending")
# PM-level agents that can have direct assignments
pm_agents = {
"main-pm",
"be-pm",
"fe-pm",
"ux-pm",
"product-owner",
"head-marketing",
"auditor",
}
for task in tasks:
# Skip already assigned tasks
if task.get("assigned_to"):
assigned_to = task.get("assigned_to")
# Handle already-assigned tasks for PM agents
if assigned_to:
agent_slug = self._resolve_agent_slug(assigned_to)
if agent_slug in pm_agents and not self._is_agent_active(agent_slug):
logger.info(
"Spawning assigned PM agent",
task_id=task.get("id"),
agent_id=agent_slug,
)
await self.spawn_agent(
agent_id=agent_slug,
task_id=task["id"],
initial_prompt=self._build_pm_triage_prompt(task),
)
continue
# Classify the task