mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Deleted Files (7,506 lines removed)
| File | Lines | Purpose | |-------------------------------|--------|-------------------------------------------| | HOMELAB_TEAM_V0.md | 3,443 | Original blueprint doc (now in CLAUDE.md) | | WORKFLOWS.md | 260 | Workflow docs | | roboco/agents/*.py | ~5,800 | Entire Python agent framework (14 files) | | roboco/models/organization.py | 157 | Unused org types | New Files (53 lines added) | File | Lines | Purpose | |-----------------------------|-------|------------------------------------------------------------| | roboco/runtime/streaming.py | 53 | Migrated set_reasoning_stream_callback from deleted agents | Modified Files Config & Settings: - .gitignore - Added .OLD/ directory Blueprints (13 files): - Fixed roboco_task_plan() signatures: (task_id, plan) → (task_id, approach, steps, risks?, open_questions?) - PM blueprints: Fixed channel access (read/write for dev-all, qa-all, doc-all) Core Code: | File | Changes | |--------------------------------|-------------------------------------------| | roboco/agents_config.py | Team naming uxui → ux_ui, docstring fixes | | roboco/api/routes/tasks.py | Hardcoded roles → AgentRole enum | | roboco/services/permissions.py | Cell PM → Main PM notification fix | | roboco/services/task.py | Removed unused imports | | roboco/bootstrap.py | Updated import path after agents deletion | | roboco/runtime/__init__.py | Added streaming exports | | roboco/runtime/orchestrator.py | Various improvements (+229/-10) | | roboco/mcp/task_server.py | Docstring team fix | | roboco/mcp/tasks/handlers/*.py | Handler improvements | | roboco/enforcement/*.py | Lifecycle enforcement updates | | roboco/db/tables.py | Table changes (+76 lines) | | roboco/models/base.py | Minor enum tweaks | | roboco/seeds/initial_data.py | Docstring team fix | Key Architecture Change: Removed the unused Python agent framework (roboco/agents/) - the system uses Docker-based Claude Code spawning via roboco/runtime/orchestrator.py instead.
This commit is contained in:
+20
-11
@@ -89,7 +89,7 @@ def _register_core_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None
|
||||
3. AVAILABLE tasks (team pool, can claim)
|
||||
|
||||
Args:
|
||||
team: Optional team filter (backend, frontend, uxui)
|
||||
team: Optional team filter (backend, frontend, ux_ui)
|
||||
|
||||
Returns:
|
||||
Dict with paused/assigned/available tasks and guidance
|
||||
@@ -406,9 +406,7 @@ def _register_developer_submit_tools(
|
||||
)
|
||||
|
||||
|
||||
def _register_qa_verdict_tools(
|
||||
mcp: FastMCP, client: ApiClient, agent_id: str
|
||||
) -> None:
|
||||
def _register_qa_verdict_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||
"""Register QA-only verdict tools (qa_pass, qa_fail)."""
|
||||
|
||||
@mcp.tool()
|
||||
@@ -453,9 +451,7 @@ def _register_qa_verdict_tools(
|
||||
return await handle_task_qa_fail(client, task_id, qa_notes, issues, agent_id)
|
||||
|
||||
|
||||
def _register_documenter_tools(
|
||||
mcp: FastMCP, client: ApiClient, agent_id: str
|
||||
) -> None:
|
||||
def _register_documenter_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||
"""Register documenter-only tools (docs_complete)."""
|
||||
|
||||
@mcp.tool()
|
||||
@@ -710,6 +706,10 @@ def _register_session_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> N
|
||||
"""
|
||||
return await handle_session_get_for_task(client, task_id, agent_id)
|
||||
|
||||
|
||||
def _register_group_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||
"""Register group management tools (Main PM only)."""
|
||||
|
||||
@mcp.tool()
|
||||
async def roboco_group_create(data: GroupCreateInput) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -770,18 +770,27 @@ def create_task_mcp_server(agent_id: str) -> FastMCP:
|
||||
# Documenters: docs completion only
|
||||
_register_documenter_tools(mcp, client, agent_id)
|
||||
|
||||
elif role in ("cell_pm", "main_pm"):
|
||||
# PMs: full management capabilities
|
||||
elif role == "cell_pm":
|
||||
# Cell PMs: task management + sessions (no group creation)
|
||||
_register_pm_completion_tools(mcp, client, agent_id)
|
||||
_register_pm_tools(mcp, client, agent_id)
|
||||
_register_session_tools(mcp, client, agent_id)
|
||||
_register_blocking_tools(mcp, client, agent_id)
|
||||
|
||||
elif role in ("product_owner", "head_marketing", "auditor", "ceo"):
|
||||
# Board/Management: PM tools + completion
|
||||
elif role == "main_pm":
|
||||
# Main PM: full management including group creation
|
||||
_register_pm_completion_tools(mcp, client, agent_id)
|
||||
_register_pm_tools(mcp, client, agent_id)
|
||||
_register_session_tools(mcp, client, agent_id)
|
||||
_register_group_tools(mcp, client, agent_id)
|
||||
_register_blocking_tools(mcp, client, agent_id)
|
||||
|
||||
elif role in ("product_owner", "head_marketing", "auditor", "ceo"):
|
||||
# Board/Management: PM tools + completion + groups
|
||||
_register_pm_completion_tools(mcp, client, agent_id)
|
||||
_register_pm_tools(mcp, client, agent_id)
|
||||
_register_session_tools(mcp, client, agent_id)
|
||||
_register_group_tools(mcp, client, agent_id)
|
||||
|
||||
# Unknown role: only core tools (scan, get, claim, etc.)
|
||||
|
||||
|
||||
@@ -39,6 +39,32 @@ async def get_available_tasks_for_role(
|
||||
return resp.json() if resp.ok else []
|
||||
|
||||
|
||||
def _get_role_pending_task_guidance(
|
||||
assigned_tasks: list[dict], agent_role: str
|
||||
) -> str | None:
|
||||
"""Get special guidance for non-standard pending tasks.
|
||||
|
||||
When QA/Documenter is directly assigned a pending task (not their usual queue),
|
||||
they need to know it's direct work - use submit_pm_review workflow.
|
||||
"""
|
||||
pending_tasks = [t for t in assigned_tasks if t.get("status") == "pending"]
|
||||
if not pending_tasks:
|
||||
return None
|
||||
|
||||
role_hints = {
|
||||
"qa": "These are NOT QA reviews",
|
||||
"documenter": "These are NOT awaiting_documentation tasks",
|
||||
}
|
||||
hint = role_hints.get(agent_role, "These are direct assignments")
|
||||
|
||||
return (
|
||||
f"You have {len(pending_tasks)} PENDING task(s) directly assigned to you. "
|
||||
f"{hint} - they are tasks assigned for YOU to complete. "
|
||||
"Workflow: claim → plan → start → work → submit_pm_review. "
|
||||
"Use roboco_task_get to see details, then roboco_task_claim to start."
|
||||
)
|
||||
|
||||
|
||||
def get_scan_guidance(
|
||||
paused_tasks: list[dict],
|
||||
assigned_tasks: list[dict],
|
||||
@@ -51,6 +77,13 @@ def get_scan_guidance(
|
||||
f"You have {len(paused_tasks)} paused task(s). "
|
||||
"Resume your paused work before claiming new tasks."
|
||||
)
|
||||
|
||||
# Special case: QA/Documenter with pending tasks (not their usual queue)
|
||||
if agent_role in ("qa", "documenter"):
|
||||
pending_guidance = _get_role_pending_task_guidance(assigned_tasks, agent_role)
|
||||
if pending_guidance:
|
||||
return pending_guidance
|
||||
|
||||
if assigned_tasks:
|
||||
return (
|
||||
f"You have {len(assigned_tasks)} active task(s). "
|
||||
@@ -66,14 +99,15 @@ def get_scan_guidance(
|
||||
|
||||
def check_blocking_tasks(active_tasks: list[dict]) -> dict[str, Any] | None:
|
||||
"""Check for blocking active tasks. Returns error or None."""
|
||||
blocking_statuses = ["claimed", "in_progress", "verifying"]
|
||||
blocking_statuses = ["pending", "claimed", "in_progress", "verifying"]
|
||||
blocking = [t for t in active_tasks if t.get("status") in blocking_statuses]
|
||||
if blocking:
|
||||
status = blocking[0].get("status", "active")
|
||||
return format_error_response(
|
||||
"ALREADY_ACTIVE",
|
||||
f"You already have an active task: {blocking[0]['id']}. "
|
||||
"Complete or pause it before claiming a new task.",
|
||||
{"active_task_id": blocking[0]["id"]},
|
||||
f"You have a {status} task: {blocking[0]['id']}. "
|
||||
"Work on it first, or pause it if blocked.",
|
||||
{"active_task_id": blocking[0]["id"], "status": status},
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -91,8 +125,14 @@ def check_paused_tasks(active_tasks: list[dict]) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
|
||||
def validate_task_claimable(task: dict, agent_role: str) -> dict[str, Any] | None:
|
||||
"""Validate task can be claimed based on agent role."""
|
||||
async def validate_task_claimable(
|
||||
task: dict, agent_role: str, agent_id: str, client: ApiClient
|
||||
) -> dict[str, Any] | None:
|
||||
"""Validate task can be claimed based on agent role.
|
||||
|
||||
Special case: If an agent is already assigned to a pending task (PM assigned
|
||||
it directly to them), they can claim it to transition to 'claimed' status.
|
||||
"""
|
||||
task_status = task.get("status")
|
||||
claimable_statuses = {
|
||||
"qa": ["awaiting_qa"],
|
||||
@@ -101,6 +141,15 @@ def validate_task_claimable(task: dict, agent_role: str) -> dict[str, Any] | Non
|
||||
}
|
||||
allowed = claimable_statuses.get(agent_role, ["pending"])
|
||||
|
||||
# Special case: agent can claim pending tasks already assigned to them
|
||||
# This handles PM directly assigning tasks to QA/docs agents
|
||||
if task_status == "pending":
|
||||
assigned_to = task.get("assigned_to")
|
||||
if assigned_to:
|
||||
agent_uuid = await resolve_agent_uuid_cached(agent_id, client)
|
||||
if agent_uuid and assigned_to == agent_uuid:
|
||||
return None # Allow claiming - already assigned to this agent
|
||||
|
||||
if task_status not in allowed:
|
||||
return format_error_response(
|
||||
"INVALID_STATE",
|
||||
@@ -148,6 +197,24 @@ def validate_task_status(
|
||||
return None
|
||||
|
||||
|
||||
def validate_task_status_in(
|
||||
task: dict[str, Any], allowed: set[str], action_desc: str
|
||||
) -> dict[str, Any] | None:
|
||||
"""Validate task is in one of the allowed statuses. Returns error or None.
|
||||
|
||||
Use this for workflow validations where multiple statuses are valid entry points.
|
||||
Example: QA can pass tasks in awaiting_qa, claimed, or in_progress status.
|
||||
"""
|
||||
task_status = task.get("status")
|
||||
if task_status not in allowed:
|
||||
return format_error_response(
|
||||
"INVALID_STATE",
|
||||
f"Can only {action_desc} tasks in {', '.join(sorted(allowed))} status. "
|
||||
f"Current: '{task_status}'",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def validate_task_ownership(
|
||||
task: dict, agent_id: str, client: ApiClient
|
||||
) -> dict[str, Any] | None:
|
||||
@@ -184,7 +251,28 @@ def validate_task_status_claimed(task: dict) -> dict[str, Any] | None:
|
||||
|
||||
|
||||
def build_plan_data(plan_params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Build the plan data structure from params."""
|
||||
"""Build the plan data structure from params.
|
||||
|
||||
Supports two formats for open_questions:
|
||||
- List of strings: ["Question 1", "Question 2"] -> sets answered=False
|
||||
- List of dicts: [{"question": "Q1", "answered": True}] -> preserves status
|
||||
"""
|
||||
raw_questions = plan_params.get("open_questions") or []
|
||||
open_questions = []
|
||||
for q in raw_questions:
|
||||
if isinstance(q, str):
|
||||
# Simple string format - new unanswered question
|
||||
open_questions.append({"question": q, "answered": False})
|
||||
elif isinstance(q, dict):
|
||||
# Dict format - preserve answered status if provided
|
||||
open_questions.append(
|
||||
{
|
||||
"question": q.get("question", ""),
|
||||
"answered": q.get("answered", False),
|
||||
"answer": q.get("answer"), # Optional: store the answer text
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"approach": plan_params["approach"],
|
||||
"sub_tasks": [
|
||||
@@ -197,10 +285,7 @@ def build_plan_data(plan_params: dict[str, Any]) -> dict[str, Any]:
|
||||
for i, st in enumerate(plan_params["sub_tasks"])
|
||||
],
|
||||
"risks": [{"description": r} for r in (plan_params.get("risks") or [])],
|
||||
"open_questions": [
|
||||
{"question": q, "answered": False}
|
||||
for q in (plan_params.get("open_questions") or [])
|
||||
],
|
||||
"open_questions": open_questions,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from roboco.agents_config import get_agent_role
|
||||
from roboco.mcp.tasks import format_task_response
|
||||
from roboco.mcp.tasks.handlers._helpers import (
|
||||
check_blocking_tasks,
|
||||
check_paused_tasks,
|
||||
fetch_task_or_error,
|
||||
get_project_context,
|
||||
validate_task_claimable,
|
||||
@@ -19,14 +18,18 @@ from roboco.mcp.utils import ApiClient, format_error_response
|
||||
|
||||
|
||||
async def _check_active_tasks(client: ApiClient) -> dict[str, Any] | None:
|
||||
"""Check for blocking or paused tasks. Returns error or None."""
|
||||
"""Check for blocking tasks. Returns error or None.
|
||||
|
||||
Note: Paused tasks no longer block claiming. Agents can verify why
|
||||
a task is paused (via roboco_task_scan) and decide to resume it
|
||||
or claim new work if it's legitimately waiting on something.
|
||||
"""
|
||||
active_resp = await client.get("/tasks/my")
|
||||
if not active_resp.ok:
|
||||
return None
|
||||
active_tasks = active_resp.json()
|
||||
if error := check_blocking_tasks(active_tasks):
|
||||
return error
|
||||
return check_paused_tasks(active_tasks)
|
||||
# Only block on in_progress tasks, not paused ones
|
||||
return check_blocking_tasks(active_tasks)
|
||||
|
||||
|
||||
async def _execute_claim(
|
||||
@@ -58,7 +61,7 @@ async def handle_task_claim(
|
||||
assert task is not None
|
||||
|
||||
agent_role = get_agent_role(agent_id)
|
||||
if error := validate_task_claimable(task, agent_role):
|
||||
if error := await validate_task_claimable(task, agent_role, agent_id, client):
|
||||
return error
|
||||
|
||||
claimed_task, error = await _execute_claim(client, task_id, agent_id)
|
||||
|
||||
@@ -10,9 +10,15 @@ from fastapi import status
|
||||
|
||||
from roboco.agents_config import can_cancel_tasks, get_agent_role
|
||||
from roboco.mcp.tasks import format_task_response
|
||||
from roboco.mcp.tasks.handlers._helpers import fetch_task_or_error, validate_task_status
|
||||
from roboco.mcp.tasks.handlers._helpers import (
|
||||
fetch_task_or_error,
|
||||
validate_task_status_in,
|
||||
)
|
||||
from roboco.mcp.utils import ApiClient, format_error_response
|
||||
|
||||
# Documenter workflow: awaiting_documentation → claim → plan → start → docs_complete
|
||||
DOCUMENTER_WORKFLOW_STATUSES = {"awaiting_documentation", "claimed", "in_progress"}
|
||||
|
||||
|
||||
def _validate_documenter_role(agent_id: str) -> dict[str, Any] | None:
|
||||
"""Validate agent is a documenter. Returns error or None."""
|
||||
@@ -50,8 +56,8 @@ async def handle_docs_complete(
|
||||
return error
|
||||
assert task is not None
|
||||
|
||||
if error := validate_task_status(
|
||||
task, "awaiting_documentation", "mark as docs complete"
|
||||
if error := validate_task_status_in(
|
||||
task, DOCUMENTER_WORKFLOW_STATUSES, "mark as docs complete"
|
||||
):
|
||||
return error
|
||||
|
||||
@@ -129,6 +135,30 @@ async def handle_task_complete(
|
||||
)
|
||||
|
||||
|
||||
def _validate_not_qa_on_dev_work(
|
||||
task: dict[str, Any], agent_id: str
|
||||
) -> dict[str, Any] | None:
|
||||
"""Validate QA agents don't bypass the proper QA workflow for dev tasks.
|
||||
|
||||
If a QA agent is working on a task that was previously developer work
|
||||
(indicated by self_verified=True), they MUST use roboco_task_qa_pass or
|
||||
roboco_task_qa_fail, not submit_pm_review.
|
||||
"""
|
||||
agent_role = get_agent_role(agent_id)
|
||||
if agent_role != "qa":
|
||||
return None # Not QA, allow
|
||||
|
||||
# Check if this is dev work that went through verification
|
||||
if task.get("self_verified"):
|
||||
return format_error_response(
|
||||
"USE_QA_TOOLS",
|
||||
"This is developer work that went through QA queue. "
|
||||
"Use roboco_task_qa_pass or roboco_task_qa_fail instead.",
|
||||
{"hint": "qa_pass sends to documenter, qa_fail returns to dev"},
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
async def handle_submit_pm_review(
|
||||
client: ApiClient, task_id: str, agent_id: str, notes: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
@@ -136,12 +166,19 @@ async def handle_submit_pm_review(
|
||||
|
||||
For tasks that don't follow the standard dev→QA→docs workflow,
|
||||
such as PM validation tasks, QA audit tasks, or directly-assigned work.
|
||||
|
||||
IMPORTANT: QA agents reviewing dev work (self_verified=True) must use
|
||||
roboco_task_qa_pass/qa_fail instead - this ensures documenter phase.
|
||||
"""
|
||||
task, error = await fetch_task_or_error(client, task_id)
|
||||
if error:
|
||||
return error
|
||||
assert task is not None
|
||||
|
||||
# QA agents reviewing dev work must use qa_pass/qa_fail
|
||||
if error := _validate_not_qa_on_dev_work(task, agent_id):
|
||||
return error
|
||||
|
||||
# Must be in_progress to submit for PM review
|
||||
current_status = task.get("status")
|
||||
if current_status != "in_progress":
|
||||
|
||||
@@ -7,15 +7,20 @@ Handlers for task verification and QA review.
|
||||
from typing import Any
|
||||
|
||||
from roboco.agents_config import get_agent_role
|
||||
from roboco.enforcement import can_review_task
|
||||
from roboco.mcp.tasks import format_task_response
|
||||
from roboco.mcp.tasks.handlers._helpers import (
|
||||
fetch_task_or_error,
|
||||
validate_task_ownership,
|
||||
validate_task_status,
|
||||
validate_task_status_in,
|
||||
)
|
||||
from roboco.mcp.utils import ApiClient, format_error_response, resolve_agent_uuid_cached
|
||||
from roboco.services.task import extract_original_developer
|
||||
|
||||
# QA workflow statuses: awaiting_qa → claim → plan → start (in_progress) → verdict
|
||||
QA_WORKFLOW_STATUSES = {"awaiting_qa", "claimed", "in_progress"}
|
||||
|
||||
|
||||
def _validate_developer_role(agent_id: str) -> dict[str, Any] | None:
|
||||
"""Validate agent is a developer (not PM/QA/Documenter). Returns error or None."""
|
||||
@@ -194,7 +199,7 @@ async def _check_self_review(
|
||||
quick_context = task.get("quick_context")
|
||||
original_dev = extract_original_developer(quick_context)
|
||||
agent_uuid = await resolve_agent_uuid_cached(agent_id, client)
|
||||
if original_dev and agent_uuid and agent_uuid == original_dev:
|
||||
if agent_uuid and not can_review_task(agent_uuid, original_dev):
|
||||
return format_error_response("SELF_REVIEW", "Cannot review your own work.")
|
||||
return None
|
||||
|
||||
@@ -211,7 +216,7 @@ async def handle_task_qa_pass(
|
||||
return error
|
||||
assert task is not None
|
||||
|
||||
if error := validate_task_status(task, "awaiting_qa", "pass QA on"):
|
||||
if error := validate_task_status_in(task, QA_WORKFLOW_STATUSES, "pass QA on"):
|
||||
return error
|
||||
|
||||
if error := await _check_self_review(task, agent_id, client):
|
||||
@@ -262,7 +267,7 @@ async def handle_task_qa_fail(
|
||||
return error
|
||||
assert task is not None
|
||||
|
||||
if error := validate_task_status(task, "awaiting_qa", "fail QA on"):
|
||||
if error := validate_task_status_in(task, QA_WORKFLOW_STATUSES, "fail QA on"):
|
||||
return error
|
||||
|
||||
full_notes = f"{qa_notes}\n\nIssues:\n" + "\n".join(f"- {i}" for i in issues)
|
||||
|
||||
Reference in New Issue
Block a user