mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Fixed issues reported by agents
This commit is contained in:
@@ -56,7 +56,7 @@ def upgrade() -> None:
|
|||||||
),
|
),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"team",
|
"team",
|
||||||
sa.Enum("backend", "frontend", "ux_ui", "board", name="team"),
|
sa.Enum("backend", "frontend", "ux_ui", "main_pm", "board", "marketing", name="team"),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
),
|
),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
@@ -126,7 +126,7 @@ def upgrade() -> None:
|
|||||||
sa.Column(
|
sa.Column(
|
||||||
"team",
|
"team",
|
||||||
sa.Enum(
|
sa.Enum(
|
||||||
"backend", "frontend", "ux_ui", "board", name="team", create_type=False
|
"backend", "frontend", "ux_ui", "main_pm", "board", "marketing", name="team", create_type=False
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
index=True,
|
index=True,
|
||||||
|
|||||||
@@ -50,6 +50,97 @@ services:
|
|||||||
entrypoint: ["/bin/sh", "-c", "echo 'Agent base image built successfully'"]
|
entrypoint: ["/bin/sh", "-c", "echo 'Agent base image built successfully'"]
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# Agent PM Image Builder (specialized image built on-demand by orchestrator)
|
||||||
|
# ==========================================================================
|
||||||
|
agent-pm-image:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/agent-pm.Dockerfile
|
||||||
|
image: roboco-agent-pm
|
||||||
|
entrypoint: ["/bin/sh", "-c", "echo 'Agent PM image built'"]
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
- agent-base-image
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# Agent Backend Dev Image Builder (specialized image built on-demand by orchestrator)
|
||||||
|
# ==========================================================================
|
||||||
|
agent-dev-be-image:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/agent-dev-be.Dockerfile
|
||||||
|
image: roboco-agent-dev-be
|
||||||
|
entrypoint: ["/bin/sh", "-c", "echo 'Agent Backend Dev image built'"]
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
- agent-base-image
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# Agent Frontend Dev Image Builder (specialized image built on-demand by orchestrator)
|
||||||
|
# ==========================================================================
|
||||||
|
agent-dev-fe-image:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/agent-dev-fe.Dockerfile
|
||||||
|
image: roboco-agent-dev-fe
|
||||||
|
entrypoint: ["/bin/sh", "-c", "echo 'Agent Frontend Dev image built'"]
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
- agent-base-image
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# Agent Backend QA Image Builder (specialized image built on-demand by orchestrator)
|
||||||
|
# ==========================================================================
|
||||||
|
agent-qa-be-image:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/agent-qa-be.Dockerfile
|
||||||
|
image: roboco-agent-qa-be
|
||||||
|
entrypoint: ["/bin/sh", "-c", 'echo "Agent Backend QA image built"']
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
- agent-base-image
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# Agent Frontend QA Image Builder (specialized image built on-demand by orchestrator)
|
||||||
|
# ==========================================================================
|
||||||
|
agent-qa-fe-image:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/agent-qa-fe.Dockerfile
|
||||||
|
image: roboco-agent-qa-fe
|
||||||
|
entrypoint: ["/bin/sh", "-c", 'echo "Agent Frontend QA image built"']
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
- agent-base-image
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# Agent UX/UI Dev Image Builder (specialized image built on-demand by orchestrator)
|
||||||
|
# ==========================================================================
|
||||||
|
agent-ux-image:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/agent-ux.Dockerfile
|
||||||
|
image: roboco-agent-ux
|
||||||
|
entrypoint: ["/bin/sh", "-c", 'echo "Agent UX/UI image built"']
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
- agent-base-image
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# Agent Documenter Image Builder (specialized image built on-demand by orchestrator)
|
||||||
|
# ==========================================================================
|
||||||
|
agent-doc-image:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/agent-doc.Dockerfile
|
||||||
|
image: roboco-agent-doc
|
||||||
|
entrypoint: ["/bin/sh", "-c", 'echo "Agent Documenter image built"']
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
- agent-base-image
|
||||||
|
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
# Orchestrator - API Server + Agent Spawner
|
# Orchestrator - API Server + Agent Spawner
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
|
|||||||
+2
-6
@@ -133,13 +133,9 @@ select = [
|
|||||||
"RUF", # Ruff-specific
|
"RUF", # Ruff-specific
|
||||||
]
|
]
|
||||||
|
|
||||||
# MCP servers: Tool functions require explicit parameters for schema generation.
|
# Lazy imports to avoid circular dependencies
|
||||||
# PLC0415 - Lazy imports for circular import avoidance
|
|
||||||
#
|
|
||||||
# Services: Internal methods often need multiple related parameters for DB ops.
|
|
||||||
# PLC0415 - Lazy imports for circular import avoidance
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"roboco/mcp/*.py" = ["PLC0415"]
|
"roboco/mcp/**/*.py" = ["PLC0415"]
|
||||||
"roboco/services/*.py" = ["PLC0415"]
|
"roboco/services/*.py" = ["PLC0415"]
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
+1
-3
@@ -500,9 +500,7 @@ Please review and provide guidance.
|
|||||||
# Check if subtasks are complete (common blocker)
|
# Check if subtasks are complete (common blocker)
|
||||||
subtasks = result.get("subtasks", [])
|
subtasks = result.get("subtasks", [])
|
||||||
if subtasks:
|
if subtasks:
|
||||||
all_complete = all(
|
all_complete = all(s.get("status") == "completed" for s in subtasks)
|
||||||
s.get("status") == "completed" for s in subtasks
|
|
||||||
)
|
|
||||||
if all_complete:
|
if all_complete:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ async def list_sessions(
|
|||||||
query = (
|
query = (
|
||||||
select(SessionTable)
|
select(SessionTable)
|
||||||
.where(SessionTable.group_id == params.group_id)
|
.where(SessionTable.group_id == params.group_id)
|
||||||
.options(selectinload(SessionTable.task_links).selectinload(SessionTaskTable.task))
|
.options(
|
||||||
|
selectinload(SessionTable.task_links).selectinload(SessionTaskTable.task)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if params.status_filter:
|
if params.status_filter:
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ async def create_task(
|
|||||||
created_by=agent.agent_id,
|
created_by=agent.agent_id,
|
||||||
priority=data.priority,
|
priority=data.priority,
|
||||||
parent_task_id=data.parent_task_id,
|
parent_task_id=data.parent_task_id,
|
||||||
|
assigned_to=data.assigned_to,
|
||||||
target_date=data.target_date,
|
target_date=data.target_date,
|
||||||
estimated_complexity=data.estimated_complexity,
|
estimated_complexity=data.estimated_complexity,
|
||||||
status=data.status,
|
status=data.status,
|
||||||
@@ -948,7 +949,7 @@ async def complete_task(
|
|||||||
incomplete_subtasks = [
|
incomplete_subtasks = [
|
||||||
st
|
st
|
||||||
for st in subtasks
|
for st in subtasks
|
||||||
if st.status not in (TaskStatus.completed, TaskStatus.cancelled)
|
if st.status not in (TaskStatus.COMPLETED, TaskStatus.CANCELLED)
|
||||||
]
|
]
|
||||||
if incomplete_subtasks:
|
if incomplete_subtasks:
|
||||||
max_titles_shown = 3
|
max_titles_shown = 3
|
||||||
|
|||||||
@@ -50,6 +50,47 @@ def _is_same_cell(agent1: str, agent2: str) -> bool:
|
|||||||
return cell1 is not None and cell1 == cell2
|
return cell1 is not None and cell1 == cell2
|
||||||
|
|
||||||
|
|
||||||
|
# Roles with global read access (can read all non-protected journals)
|
||||||
|
GLOBAL_READERS = frozenset(
|
||||||
|
["ceo", "auditor", "product_owner", "head_marketing", "main_pm"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Roles that can read cross-cell PM journals
|
||||||
|
PM_ROLES = frozenset(["cell_pm", "main_pm"])
|
||||||
|
|
||||||
|
# Cell member roles (can only read same-cell journals)
|
||||||
|
CELL_MEMBER_ROLES = frozenset(["developer", "qa", "documenter"])
|
||||||
|
|
||||||
|
|
||||||
|
def _check_protected_access(
|
||||||
|
reader_role: str, owner_id: str, owner_role: str
|
||||||
|
) -> tuple[bool, str] | None:
|
||||||
|
"""Check access to protected journals. Returns None if not protected."""
|
||||||
|
if owner_id not in PROTECTED_JOURNALS and owner_role not in ("ceo", "auditor"):
|
||||||
|
return None # Not a protected journal
|
||||||
|
if reader_role in ("ceo", "auditor"):
|
||||||
|
return True, "OK"
|
||||||
|
return False, f"Cannot read {owner_role}'s journal - protected"
|
||||||
|
|
||||||
|
|
||||||
|
def _check_cell_pm_access(
|
||||||
|
reader_id: str, owner_id: str, owner_role: str
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
"""Check Cell PM's access to another journal."""
|
||||||
|
if _is_same_cell(reader_id, owner_id):
|
||||||
|
return True, "OK"
|
||||||
|
if owner_role in PM_ROLES:
|
||||||
|
return True, "OK"
|
||||||
|
return False, "Cell PM can only read journals of cell members, other PMs"
|
||||||
|
|
||||||
|
|
||||||
|
def _check_cell_member_access(reader_id: str, owner_id: str) -> tuple[bool, str]:
|
||||||
|
"""Check cell member's access to another journal."""
|
||||||
|
if _is_same_cell(reader_id, owner_id):
|
||||||
|
return True, "OK"
|
||||||
|
return False, "You can only read journals of your cell members"
|
||||||
|
|
||||||
|
|
||||||
def can_read_journal(reader_id: str, owner_id: str) -> tuple[bool, str]:
|
def can_read_journal(reader_id: str, owner_id: str) -> tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Check if reader can access owner's journal.
|
Check if reader can access owner's journal.
|
||||||
@@ -57,59 +98,29 @@ def can_read_journal(reader_id: str, owner_id: str) -> tuple[bool, str]:
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (can_read, reason)
|
Tuple of (can_read, reason)
|
||||||
"""
|
"""
|
||||||
# Self-access always allowed
|
|
||||||
if reader_id == owner_id:
|
if reader_id == owner_id:
|
||||||
return True, "OK"
|
return True, "OK"
|
||||||
|
|
||||||
reader_role = get_agent_role(reader_id)
|
reader_role = get_agent_role(reader_id)
|
||||||
owner_role = get_agent_role(owner_id)
|
owner_role = get_agent_role(owner_id)
|
||||||
|
|
||||||
# CEO and Auditor journals are protected
|
# Check protected journals first
|
||||||
if owner_id in PROTECTED_JOURNALS or owner_role in ("ceo", "auditor"):
|
if (
|
||||||
# Only CEO can read Auditor's journal and vice versa
|
result := _check_protected_access(reader_role, owner_id, owner_role)
|
||||||
if reader_role == "ceo":
|
) is not None:
|
||||||
return True, "OK"
|
return result
|
||||||
if reader_role == "auditor":
|
|
||||||
return True, "OK"
|
|
||||||
return False, f"Cannot read {owner_role}'s journal - protected"
|
|
||||||
|
|
||||||
# CEO can read all journals (except protected, handled above)
|
# Global readers can access all non-protected journals
|
||||||
if reader_role == "ceo":
|
if reader_role in GLOBAL_READERS:
|
||||||
return True, "OK"
|
return True, "OK"
|
||||||
|
|
||||||
# Auditor has silent read access to all journals
|
# Cell PM access rules
|
||||||
if reader_role == "auditor":
|
|
||||||
return True, "OK"
|
|
||||||
|
|
||||||
# Board members can read all cell journals
|
|
||||||
if reader_role in ("product_owner", "head_marketing"):
|
|
||||||
return True, "OK"
|
|
||||||
|
|
||||||
# Main PM can read all cell journals
|
|
||||||
if reader_role == "main_pm":
|
|
||||||
return True, "OK"
|
|
||||||
|
|
||||||
# Cell PM can read:
|
|
||||||
# 1. Own cell members
|
|
||||||
# 2. Other Cell PMs
|
|
||||||
# 3. Main PM (for coordination)
|
|
||||||
if reader_role == "cell_pm":
|
if reader_role == "cell_pm":
|
||||||
# Own cell members
|
return _check_cell_pm_access(reader_id, owner_id, owner_role)
|
||||||
if _is_same_cell(reader_id, owner_id):
|
|
||||||
return True, "OK"
|
|
||||||
# Other Cell PMs
|
|
||||||
if owner_role == "cell_pm":
|
|
||||||
return True, "OK"
|
|
||||||
# Main PM
|
|
||||||
if owner_role == "main_pm":
|
|
||||||
return True, "OK"
|
|
||||||
return False, "Cell PM can only read journals of cell members, other PMs"
|
|
||||||
|
|
||||||
# Cell members (developer, qa, documenter) can read same cell journals
|
# Cell member access rules
|
||||||
if reader_role in ("developer", "qa", "documenter"):
|
if reader_role in CELL_MEMBER_ROLES:
|
||||||
if _is_same_cell(reader_id, owner_id):
|
return _check_cell_member_access(reader_id, owner_id)
|
||||||
return True, "OK"
|
|
||||||
return False, "You can only read journals of your cell members"
|
|
||||||
|
|
||||||
return False, "Unknown role - access denied"
|
return False, "Unknown role - access denied"
|
||||||
|
|
||||||
|
|||||||
@@ -352,53 +352,70 @@ def create_journal_mcp_server(agent_id: str) -> FastMCP:
|
|||||||
client = ApiClient(agent_id)
|
client = ApiClient(agent_id)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_journal_entry(data: JournalEntryInput) -> dict[str, Any]:
|
async def roboco_journal_entry(entry: JournalEntryInput) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Create a general journal entry.
|
Create a general journal entry.
|
||||||
|
|
||||||
Your journal is personal - use it to track thoughts, progress,
|
Your journal is personal - use it to track thoughts, progress,
|
||||||
and document your journey on tasks.
|
and document your journey on tasks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry: Journal entry with title, content, entry_type, task_id, tags
|
||||||
"""
|
"""
|
||||||
return await _handle_journal_entry(data, client)
|
return await _handle_journal_entry(entry, client)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_journal_reflect(data: TaskReflectionInput) -> dict[str, Any]:
|
async def roboco_journal_reflect(reflection: TaskReflectionInput) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Add a task reflection entry.
|
Add a task reflection entry.
|
||||||
|
|
||||||
IMPORTANT: Call this when completing a task. Reflections help build
|
IMPORTANT: Call this when completing a task. Reflections help build
|
||||||
institutional memory and track your growth.
|
institutional memory and track your growth.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reflection: Reflection with task_id, title, what_done, what_learned,
|
||||||
|
what_struggled, next_steps
|
||||||
"""
|
"""
|
||||||
return await _handle_reflect(data, client)
|
return await _handle_reflect(reflection, client)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_journal_decision(data: DecisionLogInput) -> dict[str, Any]:
|
async def roboco_journal_decision(decision: DecisionLogInput) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Log a decision you made.
|
Log a decision you made.
|
||||||
|
|
||||||
Use when choosing between approaches. Creates a record of WHY
|
Use when choosing between approaches. Creates a record of WHY
|
||||||
you made the decision for future context.
|
you made the decision for future context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
decision: Decision log with title, context, options, chosen, rationale
|
||||||
"""
|
"""
|
||||||
return await _handle_decision(data, client)
|
return await _handle_decision(decision, client)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_journal_learning(data: LearningInput) -> dict[str, Any]:
|
async def roboco_journal_learning(learning: LearningInput) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Log something you learned.
|
Log something you learned.
|
||||||
|
|
||||||
Track learnings to build your knowledge base and help future you.
|
Track learnings to build your knowledge base and help future you.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
learning: Learning entry with title, what_learned, how_applied, source
|
||||||
"""
|
"""
|
||||||
return await _handle_learning(data, client)
|
return await _handle_learning(learning, client)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_journal_struggle(data: StruggleInput) -> dict[str, Any]:
|
async def roboco_journal_struggle(struggle: StruggleInput) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Log a struggle or challenge.
|
Log a struggle or challenge.
|
||||||
|
|
||||||
Recording struggles helps track problem-solving patterns and
|
Recording struggles helps track problem-solving patterns and
|
||||||
create documentation for others.
|
create documentation for others.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
struggle: Struggle entry with title, what_struggled,
|
||||||
|
attempted_solutions, resolution, help_needed
|
||||||
"""
|
"""
|
||||||
return await _handle_struggle(data, client)
|
return await _handle_struggle(struggle, client)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_journal_search(query: str, top_k: int = 5) -> dict[str, Any]:
|
async def roboco_journal_search(query: str, top_k: int = 5) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -222,7 +222,20 @@ async def validate_task_start(
|
|||||||
|
|
||||||
if task_status == "claimed" and not task.get("plan"):
|
if task_status == "claimed" and not task.get("plan"):
|
||||||
return format_error_response(
|
return format_error_response(
|
||||||
"NO_PLAN", "Cannot start without a plan. Call roboco_task_plan first."
|
"NO_PLAN",
|
||||||
|
"Cannot start without a plan.",
|
||||||
|
{
|
||||||
|
"required_action": "roboco_task_plan(task_id, approach, steps)",
|
||||||
|
"workflow": "claim → PLAN → start",
|
||||||
|
"example": {
|
||||||
|
"task_id": task.get("id"),
|
||||||
|
"approach": "Describe your implementation approach",
|
||||||
|
"steps": [
|
||||||
|
{"title": "Step 1", "description": "What to do first"},
|
||||||
|
{"title": "Step 2", "description": "What to do next"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if task_status == "claimed":
|
if task_status == "claimed":
|
||||||
|
|||||||
@@ -72,10 +72,11 @@ async def handle_task_claim(
|
|||||||
|
|
||||||
return format_task_response(
|
return format_task_response(
|
||||||
claimed_task,
|
claimed_task,
|
||||||
"UNDERSTAND",
|
"PLAN",
|
||||||
"Task claimed successfully. "
|
"Task claimed. NEXT: Call roboco_task_plan() before you can start.\n"
|
||||||
"Read the description and acceptance criteria carefully. "
|
"1. Read the description and acceptance criteria\n"
|
||||||
"Ask questions if ANYTHING is unclear - do not guess. "
|
"2. Ask questions if anything is unclear\n"
|
||||||
"When ready, create your plan with roboco_task_plan.",
|
"3. Call roboco_task_plan(task_id, approach, steps)\n"
|
||||||
|
"4. Then call roboco_task_start(task_id)",
|
||||||
project=project,
|
project=project,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class Team(str, Enum):
|
|||||||
BACKEND = "backend"
|
BACKEND = "backend"
|
||||||
FRONTEND = "frontend"
|
FRONTEND = "frontend"
|
||||||
UX_UI = "ux_ui"
|
UX_UI = "ux_ui"
|
||||||
|
MAIN_PM = "main_pm" # Main PM level - cross-cell coordination
|
||||||
BOARD = "board"
|
BOARD = "board"
|
||||||
MARKETING = "marketing"
|
MARKETING = "marketing"
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ class TaskCreate(RobocoBase):
|
|||||||
team: Team
|
team: Team
|
||||||
priority: int = Field(default=2, ge=0, le=3)
|
priority: int = Field(default=2, ge=0, le=3)
|
||||||
parent_task_id: UUID | None = None
|
parent_task_id: UUID | None = None
|
||||||
|
assigned_to: UUID | None = None # Optional: assign on creation
|
||||||
target_date: datetime | None = None
|
target_date: datetime | None = None
|
||||||
estimated_complexity: Complexity = Complexity.MEDIUM
|
estimated_complexity: Complexity = Complexity.MEDIUM
|
||||||
status: TaskStatus | None = None # PM can set 'backlog' for subtasks needing setup
|
status: TaskStatus | None = None # PM can set 'backlog' for subtasks needing setup
|
||||||
@@ -269,6 +270,7 @@ class TaskCreateRequest:
|
|||||||
created_by: UUID
|
created_by: UUID
|
||||||
priority: int = 2
|
priority: int = 2
|
||||||
parent_task_id: UUID | None = None
|
parent_task_id: UUID | None = None
|
||||||
|
assigned_to: UUID | None = None
|
||||||
target_date: datetime | None = None
|
target_date: datetime | None = None
|
||||||
estimated_complexity: Complexity = field(default=Complexity.MEDIUM)
|
estimated_complexity: Complexity = field(default=Complexity.MEDIUM)
|
||||||
status: TaskStatus | None = None # PM can set BACKLOG for subtasks
|
status: TaskStatus | None = None # PM can set BACKLOG for subtasks
|
||||||
|
|||||||
@@ -77,10 +77,12 @@ AGENT_IMAGES: dict[str, str] = {
|
|||||||
"auditor": "roboco-agent-pm",
|
"auditor": "roboco-agent-pm",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_agent_image(agent_id: str) -> str:
|
def get_agent_image(agent_id: str) -> str:
|
||||||
"""Get the Docker image for an agent."""
|
"""Get the Docker image for an agent."""
|
||||||
return AGENT_IMAGES.get(agent_id, AGENT_BASE_IMAGE)
|
return AGENT_IMAGES.get(agent_id, AGENT_BASE_IMAGE)
|
||||||
|
|
||||||
|
|
||||||
# When running in a container, we need host paths for volume mounts.
|
# When running in a container, we need host paths for volume mounts.
|
||||||
# These can be overridden via environment variables.
|
# These can be overridden via environment variables.
|
||||||
CLAUDE_AUTH_HOST_PATH = os.environ.get(
|
CLAUDE_AUTH_HOST_PATH = os.environ.get(
|
||||||
@@ -665,14 +667,25 @@ class AgentOrchestrator:
|
|||||||
}
|
}
|
||||||
return role_map.get(agent_id, agent_id)
|
return role_map.get(agent_id, agent_id)
|
||||||
|
|
||||||
|
# Static team mappings for management agents
|
||||||
|
_AGENT_TEAM_MAP: ClassVar[dict[str, str]] = {
|
||||||
|
"main-pm": "main_pm",
|
||||||
|
"product-owner": "board",
|
||||||
|
"auditor": "board",
|
||||||
|
"head-marketing": "marketing",
|
||||||
|
}
|
||||||
|
|
||||||
def _get_agent_team(self, agent_id: str) -> str | None:
|
def _get_agent_team(self, agent_id: str) -> str | None:
|
||||||
"""Get team from agent_id."""
|
"""Get team from agent_id."""
|
||||||
if agent_id.startswith("be-"):
|
# Check static mappings first
|
||||||
return "backend"
|
if agent_id in self._AGENT_TEAM_MAP:
|
||||||
if agent_id.startswith("fe-"):
|
return self._AGENT_TEAM_MAP[agent_id]
|
||||||
return "frontend"
|
|
||||||
if agent_id.startswith("ux-"):
|
# Check cell prefixes
|
||||||
return "ux_ui"
|
prefix_map = {"be-": "backend", "fe-": "frontend", "ux-": "ux_ui"}
|
||||||
|
for prefix, team in prefix_map.items():
|
||||||
|
if agent_id.startswith(prefix):
|
||||||
|
return team
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _resolve_agent_slug(self, agent_id_or_uuid: str) -> str:
|
def _resolve_agent_slug(self, agent_id_or_uuid: str) -> str:
|
||||||
@@ -1150,17 +1163,30 @@ Start by:
|
|||||||
"""Check if text contains PM coordination keywords."""
|
"""Check if text contains PM coordination keywords."""
|
||||||
return any(kw in text for kw in self._PM_KEYWORDS)
|
return any(kw in text for kw in self._PM_KEYWORDS)
|
||||||
|
|
||||||
|
# Direct team-to-routing mappings (explicit assignments bypass keyword analysis)
|
||||||
|
_TEAM_ROUTING_MAP: ClassVar[dict[str, str]] = {
|
||||||
|
"main_pm": "main_pm",
|
||||||
|
"board": "board",
|
||||||
|
"marketing": "marketing",
|
||||||
|
}
|
||||||
|
|
||||||
def _classify_task_routing(self, task: dict[str, Any]) -> str:
|
def _classify_task_routing(self, task: dict[str, Any]) -> str:
|
||||||
"""
|
"""
|
||||||
Classify a task for routing based on complexity, keywords, and team.
|
Classify a task for routing based on team, complexity, and keywords.
|
||||||
|
|
||||||
Returns one of: "board", "main_pm", "cell_pm", "dev"
|
Returns one of: "board", "main_pm", "cell_pm", "dev", "marketing"
|
||||||
"""
|
"""
|
||||||
|
team = task.get("team")
|
||||||
|
|
||||||
|
# Explicit team assignment takes precedence
|
||||||
|
if team in self._TEAM_ROUTING_MAP:
|
||||||
|
return self._TEAM_ROUTING_MAP[team]
|
||||||
|
|
||||||
|
# For cell teams, use keyword/complexity analysis
|
||||||
title = (task.get("title") or "").lower()
|
title = (task.get("title") or "").lower()
|
||||||
description = (task.get("description") or "").lower()
|
description = (task.get("description") or "").lower()
|
||||||
text = f"{title} {description}"
|
text = f"{title} {description}"
|
||||||
complexity = task.get("complexity", "medium").lower()
|
complexity = task.get("estimated_complexity", "medium").lower()
|
||||||
team = task.get("team")
|
|
||||||
|
|
||||||
# Board-level keywords → Board
|
# Board-level keywords → Board
|
||||||
if self._has_board_keywords(text):
|
if self._has_board_keywords(text):
|
||||||
@@ -1189,7 +1215,7 @@ Start by:
|
|||||||
Resolve a routing decision to a specific agent slug.
|
Resolve a routing decision to a specific agent slug.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
routing: One of "board", "main_pm", "cell_pm", "dev"
|
routing: One of "board", "main_pm", "cell_pm", "dev", "marketing"
|
||||||
task: The task being routed
|
task: The task being routed
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -1198,7 +1224,11 @@ Start by:
|
|||||||
team = task.get("team")
|
team = task.get("team")
|
||||||
|
|
||||||
# Static routing targets
|
# Static routing targets
|
||||||
static_targets = {"board": "product-owner", "main_pm": "main-pm"}
|
static_targets = {
|
||||||
|
"board": "product-owner",
|
||||||
|
"main_pm": "main-pm",
|
||||||
|
"marketing": "head-marketing",
|
||||||
|
}
|
||||||
if routing in static_targets:
|
if routing in static_targets:
|
||||||
return static_targets[routing]
|
return static_targets[routing]
|
||||||
|
|
||||||
|
|||||||
@@ -237,28 +237,28 @@ DEFAULT_AGENTS: list[dict[str, Any]] = [
|
|||||||
"slug": "main-pm",
|
"slug": "main-pm",
|
||||||
"name": "Main PM",
|
"name": "Main PM",
|
||||||
"role": "main_pm",
|
"role": "main_pm",
|
||||||
"team": None,
|
"team": "main_pm", # Cross-cell coordination
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": AGENT_UUIDS["product-owner"],
|
"id": AGENT_UUIDS["product-owner"],
|
||||||
"slug": "product-owner",
|
"slug": "product-owner",
|
||||||
"name": "Product Owner",
|
"name": "Product Owner",
|
||||||
"role": "product_owner",
|
"role": "product_owner",
|
||||||
"team": None,
|
"team": "board",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": AGENT_UUIDS["head-marketing"],
|
"id": AGENT_UUIDS["head-marketing"],
|
||||||
"slug": "head-marketing",
|
"slug": "head-marketing",
|
||||||
"name": "Head of Marketing",
|
"name": "Head of Marketing",
|
||||||
"role": "head_marketing",
|
"role": "head_marketing",
|
||||||
"team": None,
|
"team": "marketing",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": AGENT_UUIDS["auditor"],
|
"id": AGENT_UUIDS["auditor"],
|
||||||
"slug": "auditor",
|
"slug": "auditor",
|
||||||
"name": "Auditor",
|
"name": "Auditor",
|
||||||
"role": "auditor",
|
"role": "auditor",
|
||||||
"team": None,
|
"team": "board", # Silent observer, board-level access
|
||||||
},
|
},
|
||||||
# CEO (Human)
|
# CEO (Human)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -220,12 +220,12 @@ async def resolve_agent_identity(
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(AgentTable.id).where(AgentTable.slug == agent_id_or_slug)
|
select(AgentTable.id).where(AgentTable.slug == agent_id_or_slug)
|
||||||
)
|
)
|
||||||
agent_uuid = result.scalar_one_or_none()
|
found_id = result.scalar_one_or_none()
|
||||||
|
|
||||||
if agent_uuid is None:
|
if found_id is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return (UUID(str(agent_uuid)), agent_id_or_slug)
|
return (UUID(str(found_id)), agent_id_or_slug)
|
||||||
|
|
||||||
|
|
||||||
async def get_agent_slug(
|
async def get_agent_slug(
|
||||||
@@ -242,9 +242,7 @@ async def get_agent_slug(
|
|||||||
Returns:
|
Returns:
|
||||||
The agent's slug (e.g., "be-dev-1"), or None if not found
|
The agent's slug (e.g., "be-dev-1"), or None if not found
|
||||||
"""
|
"""
|
||||||
result = await db.execute(
|
result = await db.execute(select(AgentTable.slug).where(AgentTable.id == agent_id))
|
||||||
select(AgentTable.slug).where(AgentTable.id == agent_id)
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+23
-1
@@ -127,6 +127,7 @@ class TaskService(BaseService):
|
|||||||
acceptance_criteria=req.acceptance_criteria,
|
acceptance_criteria=req.acceptance_criteria,
|
||||||
team=req.team,
|
team=req.team,
|
||||||
created_by=req.created_by,
|
created_by=req.created_by,
|
||||||
|
assigned_to=req.assigned_to,
|
||||||
priority=req.priority,
|
priority=req.priority,
|
||||||
parent_task_id=req.parent_task_id,
|
parent_task_id=req.parent_task_id,
|
||||||
target_date=req.target_date,
|
target_date=req.target_date,
|
||||||
@@ -322,16 +323,37 @@ class TaskService(BaseService):
|
|||||||
return "invalid status for role"
|
return "invalid status for role"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Management roles that can claim tasks from any team
|
||||||
|
_MANAGEMENT_ROLES = frozenset(
|
||||||
|
{"main_pm", "product_owner", "head_marketing", "auditor"}
|
||||||
|
)
|
||||||
|
|
||||||
def _validate_claim_team(
|
def _validate_claim_team(
|
||||||
self, task: TaskTable, agent: AgentTable | None
|
self, task: TaskTable, agent: AgentTable | None
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Validate agent belongs to task's team.
|
Validate agent belongs to task's team.
|
||||||
|
|
||||||
|
Management roles (main_pm, product_owner, head_marketing, auditor)
|
||||||
|
can claim tasks from any team.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Error message if invalid, None if valid
|
Error message if invalid, None if valid
|
||||||
"""
|
"""
|
||||||
if agent and task.team and agent.team != task.team:
|
if not agent or not task.team:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get agent role as string
|
||||||
|
agent_role = (
|
||||||
|
agent.role.value if hasattr(agent.role, "value") else str(agent.role)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Management roles can claim any task
|
||||||
|
if agent_role in self._MANAGEMENT_ROLES:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Regular agents must match team
|
||||||
|
if agent.team != task.team:
|
||||||
return "agent not in task's team"
|
return "agent not in task's team"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user