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:
Renn F
2025-12-25 21:01:12 +01:00
parent 7f13e10bf4
commit 1f6c099d8a
55 changed files with 1397 additions and 10895 deletions
+20 -11
View File
@@ -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.)
+96 -11
View File
@@ -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,
}
+9 -6
View File
@@ -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)
+40 -3
View File
@@ -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":
+8 -3
View File
@@ -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)