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(
|
||||
"team",
|
||||
sa.Enum("backend", "frontend", "ux_ui", "board", name="team"),
|
||||
sa.Enum("backend", "frontend", "ux_ui", "main_pm", "board", "marketing", name="team"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
@@ -126,7 +126,7 @@ def upgrade() -> None:
|
||||
sa.Column(
|
||||
"team",
|
||||
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,
|
||||
index=True,
|
||||
|
||||
@@ -50,6 +50,97 @@ services:
|
||||
entrypoint: ["/bin/sh", "-c", "echo 'Agent base image built successfully'"]
|
||||
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
|
||||
# ==========================================================================
|
||||
|
||||
+2
-6
@@ -133,13 +133,9 @@ select = [
|
||||
"RUF", # Ruff-specific
|
||||
]
|
||||
|
||||
# MCP servers: Tool functions require explicit parameters for schema generation.
|
||||
# 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
|
||||
# Lazy imports to avoid circular dependencies
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"roboco/mcp/*.py" = ["PLC0415"]
|
||||
"roboco/mcp/**/*.py" = ["PLC0415"]
|
||||
"roboco/services/*.py" = ["PLC0415"]
|
||||
|
||||
# =============================================================================
|
||||
|
||||
+1
-3
@@ -500,9 +500,7 @@ Please review and provide guidance.
|
||||
# Check if subtasks are complete (common blocker)
|
||||
subtasks = result.get("subtasks", [])
|
||||
if subtasks:
|
||||
all_complete = all(
|
||||
s.get("status") == "completed" for s in subtasks
|
||||
)
|
||||
all_complete = all(s.get("status") == "completed" for s in subtasks)
|
||||
if all_complete:
|
||||
return True
|
||||
|
||||
|
||||
@@ -92,7 +92,9 @@ async def list_sessions(
|
||||
query = (
|
||||
select(SessionTable)
|
||||
.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:
|
||||
|
||||
@@ -90,6 +90,7 @@ async def create_task(
|
||||
created_by=agent.agent_id,
|
||||
priority=data.priority,
|
||||
parent_task_id=data.parent_task_id,
|
||||
assigned_to=data.assigned_to,
|
||||
target_date=data.target_date,
|
||||
estimated_complexity=data.estimated_complexity,
|
||||
status=data.status,
|
||||
@@ -948,7 +949,7 @@ async def complete_task(
|
||||
incomplete_subtasks = [
|
||||
st
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
# 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]:
|
||||
"""
|
||||
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:
|
||||
Tuple of (can_read, reason)
|
||||
"""
|
||||
# Self-access always allowed
|
||||
if reader_id == owner_id:
|
||||
return True, "OK"
|
||||
|
||||
reader_role = get_agent_role(reader_id)
|
||||
owner_role = get_agent_role(owner_id)
|
||||
|
||||
# CEO and Auditor journals are protected
|
||||
if owner_id in PROTECTED_JOURNALS or owner_role in ("ceo", "auditor"):
|
||||
# Only CEO can read Auditor's journal and vice versa
|
||||
if reader_role == "ceo":
|
||||
return True, "OK"
|
||||
if reader_role == "auditor":
|
||||
return True, "OK"
|
||||
return False, f"Cannot read {owner_role}'s journal - protected"
|
||||
# Check protected journals first
|
||||
if (
|
||||
result := _check_protected_access(reader_role, owner_id, owner_role)
|
||||
) is not None:
|
||||
return result
|
||||
|
||||
# CEO can read all journals (except protected, handled above)
|
||||
if reader_role == "ceo":
|
||||
# Global readers can access all non-protected journals
|
||||
if reader_role in GLOBAL_READERS:
|
||||
return True, "OK"
|
||||
|
||||
# Auditor has silent read access to all journals
|
||||
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)
|
||||
# Cell PM access rules
|
||||
if reader_role == "cell_pm":
|
||||
# Own cell members
|
||||
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"
|
||||
return _check_cell_pm_access(reader_id, owner_id, owner_role)
|
||||
|
||||
# Cell members (developer, qa, documenter) can read same cell journals
|
||||
if reader_role in ("developer", "qa", "documenter"):
|
||||
if _is_same_cell(reader_id, owner_id):
|
||||
return True, "OK"
|
||||
return False, "You can only read journals of your cell members"
|
||||
# Cell member access rules
|
||||
if reader_role in CELL_MEMBER_ROLES:
|
||||
return _check_cell_member_access(reader_id, owner_id)
|
||||
|
||||
return False, "Unknown role - access denied"
|
||||
|
||||
|
||||
@@ -352,53 +352,70 @@ def create_journal_mcp_server(agent_id: str) -> FastMCP:
|
||||
client = ApiClient(agent_id)
|
||||
|
||||
@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.
|
||||
|
||||
Your journal is personal - use it to track thoughts, progress,
|
||||
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()
|
||||
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.
|
||||
|
||||
IMPORTANT: Call this when completing a task. Reflections help build
|
||||
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()
|
||||
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.
|
||||
|
||||
Use when choosing between approaches. Creates a record of WHY
|
||||
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()
|
||||
async def roboco_journal_learning(data: LearningInput) -> dict[str, Any]:
|
||||
async def roboco_journal_learning(learning: LearningInput) -> dict[str, Any]:
|
||||
"""
|
||||
Log something you learned.
|
||||
|
||||
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()
|
||||
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.
|
||||
|
||||
Recording struggles helps track problem-solving patterns and
|
||||
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()
|
||||
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"):
|
||||
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":
|
||||
|
||||
@@ -72,10 +72,11 @@ async def handle_task_claim(
|
||||
|
||||
return format_task_response(
|
||||
claimed_task,
|
||||
"UNDERSTAND",
|
||||
"Task claimed successfully. "
|
||||
"Read the description and acceptance criteria carefully. "
|
||||
"Ask questions if ANYTHING is unclear - do not guess. "
|
||||
"When ready, create your plan with roboco_task_plan.",
|
||||
"PLAN",
|
||||
"Task claimed. NEXT: Call roboco_task_plan() before you can start.\n"
|
||||
"1. Read the description and acceptance criteria\n"
|
||||
"2. Ask questions if anything is unclear\n"
|
||||
"3. Call roboco_task_plan(task_id, approach, steps)\n"
|
||||
"4. Then call roboco_task_start(task_id)",
|
||||
project=project,
|
||||
)
|
||||
|
||||
@@ -48,6 +48,7 @@ class Team(str, Enum):
|
||||
BACKEND = "backend"
|
||||
FRONTEND = "frontend"
|
||||
UX_UI = "ux_ui"
|
||||
MAIN_PM = "main_pm" # Main PM level - cross-cell coordination
|
||||
BOARD = "board"
|
||||
MARKETING = "marketing"
|
||||
|
||||
|
||||
@@ -232,6 +232,7 @@ class TaskCreate(RobocoBase):
|
||||
team: Team
|
||||
priority: int = Field(default=2, ge=0, le=3)
|
||||
parent_task_id: UUID | None = None
|
||||
assigned_to: UUID | None = None # Optional: assign on creation
|
||||
target_date: datetime | None = None
|
||||
estimated_complexity: Complexity = Complexity.MEDIUM
|
||||
status: TaskStatus | None = None # PM can set 'backlog' for subtasks needing setup
|
||||
@@ -269,6 +270,7 @@ class TaskCreateRequest:
|
||||
created_by: UUID
|
||||
priority: int = 2
|
||||
parent_task_id: UUID | None = None
|
||||
assigned_to: UUID | None = None
|
||||
target_date: datetime | None = None
|
||||
estimated_complexity: Complexity = field(default=Complexity.MEDIUM)
|
||||
status: TaskStatus | None = None # PM can set BACKLOG for subtasks
|
||||
|
||||
@@ -77,10 +77,12 @@ AGENT_IMAGES: dict[str, str] = {
|
||||
"auditor": "roboco-agent-pm",
|
||||
}
|
||||
|
||||
|
||||
def get_agent_image(agent_id: str) -> str:
|
||||
"""Get the Docker image for an agent."""
|
||||
return AGENT_IMAGES.get(agent_id, AGENT_BASE_IMAGE)
|
||||
|
||||
|
||||
# When running in a container, we need host paths for volume mounts.
|
||||
# These can be overridden via environment variables.
|
||||
CLAUDE_AUTH_HOST_PATH = os.environ.get(
|
||||
@@ -665,14 +667,25 @@ class AgentOrchestrator:
|
||||
}
|
||||
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:
|
||||
"""Get team from agent_id."""
|
||||
if agent_id.startswith("be-"):
|
||||
return "backend"
|
||||
if agent_id.startswith("fe-"):
|
||||
return "frontend"
|
||||
if agent_id.startswith("ux-"):
|
||||
return "ux_ui"
|
||||
# Check static mappings first
|
||||
if agent_id in self._AGENT_TEAM_MAP:
|
||||
return self._AGENT_TEAM_MAP[agent_id]
|
||||
|
||||
# Check cell prefixes
|
||||
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
|
||||
|
||||
def _resolve_agent_slug(self, agent_id_or_uuid: str) -> str:
|
||||
@@ -1150,17 +1163,30 @@ Start by:
|
||||
"""Check if text contains PM coordination 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:
|
||||
"""
|
||||
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()
|
||||
description = (task.get("description") or "").lower()
|
||||
text = f"{title} {description}"
|
||||
complexity = task.get("complexity", "medium").lower()
|
||||
team = task.get("team")
|
||||
complexity = task.get("estimated_complexity", "medium").lower()
|
||||
|
||||
# Board-level keywords → Board
|
||||
if self._has_board_keywords(text):
|
||||
@@ -1189,7 +1215,7 @@ Start by:
|
||||
Resolve a routing decision to a specific agent slug.
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
@@ -1198,7 +1224,11 @@ Start by:
|
||||
team = task.get("team")
|
||||
|
||||
# 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:
|
||||
return static_targets[routing]
|
||||
|
||||
|
||||
@@ -237,28 +237,28 @@ DEFAULT_AGENTS: list[dict[str, Any]] = [
|
||||
"slug": "main-pm",
|
||||
"name": "Main PM",
|
||||
"role": "main_pm",
|
||||
"team": None,
|
||||
"team": "main_pm", # Cross-cell coordination
|
||||
},
|
||||
{
|
||||
"id": AGENT_UUIDS["product-owner"],
|
||||
"slug": "product-owner",
|
||||
"name": "Product Owner",
|
||||
"role": "product_owner",
|
||||
"team": None,
|
||||
"team": "board",
|
||||
},
|
||||
{
|
||||
"id": AGENT_UUIDS["head-marketing"],
|
||||
"slug": "head-marketing",
|
||||
"name": "Head of Marketing",
|
||||
"role": "head_marketing",
|
||||
"team": None,
|
||||
"team": "marketing",
|
||||
},
|
||||
{
|
||||
"id": AGENT_UUIDS["auditor"],
|
||||
"slug": "auditor",
|
||||
"name": "Auditor",
|
||||
"role": "auditor",
|
||||
"team": None,
|
||||
"team": "board", # Silent observer, board-level access
|
||||
},
|
||||
# CEO (Human)
|
||||
{
|
||||
|
||||
@@ -220,12 +220,12 @@ async def resolve_agent_identity(
|
||||
result = await db.execute(
|
||||
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 (UUID(str(agent_uuid)), agent_id_or_slug)
|
||||
return (UUID(str(found_id)), agent_id_or_slug)
|
||||
|
||||
|
||||
async def get_agent_slug(
|
||||
@@ -242,9 +242,7 @@ async def get_agent_slug(
|
||||
Returns:
|
||||
The agent's slug (e.g., "be-dev-1"), or None if not found
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(AgentTable.slug).where(AgentTable.id == agent_id)
|
||||
)
|
||||
result = await db.execute(select(AgentTable.slug).where(AgentTable.id == agent_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
|
||||
+23
-1
@@ -127,6 +127,7 @@ class TaskService(BaseService):
|
||||
acceptance_criteria=req.acceptance_criteria,
|
||||
team=req.team,
|
||||
created_by=req.created_by,
|
||||
assigned_to=req.assigned_to,
|
||||
priority=req.priority,
|
||||
parent_task_id=req.parent_task_id,
|
||||
target_date=req.target_date,
|
||||
@@ -322,16 +323,37 @@ class TaskService(BaseService):
|
||||
return "invalid status for role"
|
||||
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(
|
||||
self, task: TaskTable, agent: AgentTable | None
|
||||
) -> str | None:
|
||||
"""
|
||||
Validate agent belongs to task's team.
|
||||
|
||||
Management roles (main_pm, product_owner, head_marketing, auditor)
|
||||
can claim tasks from any team.
|
||||
|
||||
Returns:
|
||||
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 None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user