mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Documenter is ignored but it works pretty well
This commit is contained in:
+2
-7
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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})
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user