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:
@@ -82,3 +82,4 @@ alembic/versions/*.pyc
|
||||
/docs
|
||||
/data
|
||||
/#recycle
|
||||
.OLD/
|
||||
-3443
File diff suppressed because it is too large
Load Diff
-260
@@ -1,260 +0,0 @@
|
||||
# RoboCo Workflows & Permissions
|
||||
|
||||
Visual documentation of task lifecycles, permissions, and workflows.
|
||||
|
||||
## 1. Task Lifecycle State Machine
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending: Task Created
|
||||
|
||||
pending --> claimed: Developer claims
|
||||
pending --> cancelled: PM cancels
|
||||
|
||||
claimed --> in_progress: Developer starts
|
||||
claimed --> pending: Developer unclaims
|
||||
claimed --> cancelled: PM cancels
|
||||
|
||||
in_progress --> blocked: Developer blocked
|
||||
in_progress --> paused: Developer pauses
|
||||
in_progress --> verifying: Developer self-verifies
|
||||
in_progress --> cancelled: PM cancels
|
||||
|
||||
blocked --> in_progress: Unblocked
|
||||
blocked --> cancelled: PM cancels
|
||||
|
||||
paused --> in_progress: Developer resumes
|
||||
paused --> cancelled: PM cancels
|
||||
|
||||
verifying --> awaiting_qa: Submit for QA
|
||||
verifying --> needs_revision: Self-found issues
|
||||
verifying --> awaiting_documentation: Skip QA (small tasks)
|
||||
verifying --> cancelled: PM cancels
|
||||
|
||||
awaiting_qa --> awaiting_documentation: QA PASS
|
||||
awaiting_qa --> needs_revision: QA FAIL
|
||||
awaiting_qa --> blocked: Blocked during QA
|
||||
awaiting_qa --> cancelled: PM cancels
|
||||
|
||||
needs_revision --> in_progress: Developer resumes
|
||||
needs_revision --> cancelled: PM cancels
|
||||
|
||||
awaiting_documentation --> awaiting_pm_review: Documenter marks docs done
|
||||
awaiting_documentation --> cancelled: PM cancels
|
||||
|
||||
awaiting_pm_review --> completed: PM completes
|
||||
awaiting_pm_review --> cancelled: PM cancels
|
||||
|
||||
completed --> [*]
|
||||
cancelled --> [*]
|
||||
|
||||
quarantined --> pending: Un-quarantined
|
||||
```
|
||||
|
||||
## 2. Agent Hierarchy & Roles
|
||||
|
||||
```
|
||||
+-------+
|
||||
| CEO |
|
||||
+-------+
|
||||
|
|
||||
+---------------+---------------+
|
||||
| | |
|
||||
+---------+ +-----------+ +---------+
|
||||
| Product | | Head | | Auditor |
|
||||
| Owner | | Marketing | | (silent)|
|
||||
+---------+ +-----------+ +---------+
|
||||
| | |
|
||||
+-------+-------+ |
|
||||
| |
|
||||
+---------+ |
|
||||
| Main PM |<-----------------+
|
||||
+---------+ (observes all)
|
||||
|
|
||||
+-----------+-----------+
|
||||
| | |
|
||||
+-------+ +-------+ +-------+
|
||||
| BE PM | | FE PM | | UX PM |
|
||||
+-------+ +-------+ +-------+
|
||||
| | |
|
||||
+-------+ +-------+ +-------+
|
||||
|Backend| |Frontend| | UX/UI |
|
||||
| Cell | | Cell | | Cell |
|
||||
+-------+ +-------+ +-------+
|
||||
|
||||
Each Cell:
|
||||
- 2 Developers (BE/FE) or 1 Developer (UX)
|
||||
- 1 QA Engineer
|
||||
- 1 Documenter
|
||||
- 1 Cell PM
|
||||
```
|
||||
|
||||
## 3. Notification Permissions
|
||||
|
||||
```
|
||||
WHO CAN SEND NOTIFICATIONS:
|
||||
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
| Sender Role | Can Send? | Scope |
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
| CEO | YES | Anyone |
|
||||
| Auditor | YES | Anyone |
|
||||
| Main PM | YES | Anyone |
|
||||
| Product Owner | YES | main-pm, head-marketing, auditor, ceo |
|
||||
| Head Marketing | YES | main-pm, product-owner, auditor, ceo |
|
||||
| Cell PM | YES | Own cell only |
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
| Developer | NO | - |
|
||||
| QA | NO | - |
|
||||
| Documenter | NO | - |
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
|
||||
TOOLS VISIBILITY:
|
||||
|
||||
+----------------------+------------+----------+---------+---------+---------+
|
||||
| Tool | Dev/QA/Doc | Cell PM | Main PM | Board | Aud/CEO |
|
||||
+----------------------+------------+----------+---------+---------+---------+
|
||||
| roboco_notify_list | YES | YES | YES | YES | YES |
|
||||
| roboco_notify_get | YES | YES | YES | YES | YES |
|
||||
| roboco_notify_ack | YES | YES | YES | YES | YES |
|
||||
| roboco_notify_send | HIDDEN | YES | YES | YES | YES |
|
||||
| roboco_escalate | HIDDEN | YES | YES | HIDDEN | HIDDEN |
|
||||
| roboco_request_appr | HIDDEN | YES | YES | YES | HIDDEN |
|
||||
+----------------------+------------+----------+---------+---------+---------+
|
||||
|
||||
Note: "Board" = Product Owner + Head Marketing. Auditor/CEO can send but not escalate or request approval.
|
||||
```
|
||||
|
||||
## 4. QA Fail → Revision Workflow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
participant Task as Task System
|
||||
participant QA as QA Engineer
|
||||
participant PM as Cell PM
|
||||
|
||||
Dev->>Task: Submit for QA (awaiting_qa)
|
||||
Note over Task: assigned_to = QA<br/>quick_context = original_developer:Dev
|
||||
|
||||
QA->>Task: Claim task
|
||||
QA->>Task: Review work
|
||||
|
||||
alt QA PASS
|
||||
QA->>Task: roboco_task_qa_pass()
|
||||
Task->>Task: status = awaiting_documentation
|
||||
Note over Task: Documenter claims and writes docs
|
||||
Note over Task: Documenter calls roboco_task_docs_complete()
|
||||
Task->>Task: status = awaiting_pm_review
|
||||
Note over Task: PM claims, reviews, calls roboco_task_complete()
|
||||
Task->>Task: status = completed
|
||||
else QA FAIL
|
||||
QA->>Task: roboco_task_qa_fail(issues)
|
||||
Task->>Task: status = needs_revision
|
||||
Task->>Task: assigned_to = original Dev (from quick_context)
|
||||
Note over Dev: Dev sees task in needs_revision
|
||||
Dev->>Task: roboco_task_start()
|
||||
Task->>Task: status = in_progress
|
||||
Dev->>Task: Fix issues, resubmit
|
||||
end
|
||||
```
|
||||
|
||||
## 5. Block/Unblock Workflow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
participant Task as Task System
|
||||
participant PM as Cell PM
|
||||
|
||||
Dev->>Task: Working on task (in_progress)
|
||||
|
||||
Note over Dev: Encounters blocker
|
||||
|
||||
Dev->>Task: roboco_task_block(reason, type, what_needed)
|
||||
Task->>Task: POST /tasks/{id}/soft-block
|
||||
Task->>Task: status = blocked
|
||||
Task->>Task: dev_notes += blocker info
|
||||
|
||||
Note over Dev: Can work on other tasks
|
||||
|
||||
alt Blocker resolved
|
||||
Dev->>Task: roboco_task_unblock()
|
||||
Task->>Task: POST /tasks/{id}/unblock
|
||||
Task->>Task: status = in_progress
|
||||
Dev->>Task: Continue working
|
||||
else Need PM help
|
||||
Dev->>PM: roboco_report_blocker() via message channel
|
||||
PM->>Task: Resolves blocker
|
||||
Dev->>Task: roboco_task_unblock()
|
||||
end
|
||||
```
|
||||
|
||||
## 6. Task Role Restrictions
|
||||
|
||||
```
|
||||
ROLE-BASED TRANSITIONS:
|
||||
|
||||
+-------------------------------+-------------------------------------------+
|
||||
| Transition | Allowed Roles |
|
||||
+-------------------------------+-------------------------------------------+
|
||||
| awaiting_qa → awaiting_doc | QA only |
|
||||
| awaiting_qa → needs_rev | QA only |
|
||||
| awaiting_doc → awaiting_pm | Documenter only |
|
||||
| awaiting_pm → completed | Cell PM, Main PM, Product Owner, Head Mkt |
|
||||
| * → cancelled | Cell PM, Main PM, Product Owner, Head Mkt |
|
||||
+-------------------------------+-------------------------------------------+
|
||||
|
||||
Note: CEO and Auditor are NOT in the cancel/complete roles list - they observe but don't directly act on tasks.
|
||||
|
||||
VALID START STATUSES (for roboco_task_start):
|
||||
|
||||
+------------------+------------------------------------------+
|
||||
| Status | Who Can Start |
|
||||
+------------------+------------------------------------------+
|
||||
| claimed | Assigned developer (requires plan) |
|
||||
| paused | Assigned developer (resume) |
|
||||
| needs_revision | Original developer (fix QA issues) |
|
||||
+------------------+------------------------------------------+
|
||||
```
|
||||
|
||||
## 7. Escalation Chain
|
||||
|
||||
```
|
||||
Developer/QA/Doc → Cell PM → Main PM → Product Owner → CEO
|
||||
|
||||
+------------+ +---------+ +---------+ +---------------+ +-----+
|
||||
| be-dev-1 |---->| | | | | | | |
|
||||
| be-dev-2 |---->| be-pm |---->| | | | | |
|
||||
| be-qa |---->| | | | | | | |
|
||||
| be-doc |---->| | | | | | | |
|
||||
+------------+ +---------+ | | | | | |
|
||||
| main-pm |---->| product-owner |---->| CEO |
|
||||
+------------+ +---------+ | | | | | |
|
||||
| fe-dev-1 |---->| | | | | | | |
|
||||
| fe-dev-2 |---->| fe-pm |---->| | | | | |
|
||||
| fe-qa |---->| | | | | | | |
|
||||
| fe-doc |---->| | | | | | | |
|
||||
+------------+ +---------+ +---------+ +---------------+ +-----+
|
||||
```
|
||||
|
||||
## 8. Communication vs Notification
|
||||
|
||||
```
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Mechanism | Who Can Use | Purpose |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Messages | Everyone | Constant stream, logged |
|
||||
| (roboco_message) | | discussions, updates |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Blocker Reports | Everyone | Signal blocked status |
|
||||
| (roboco_report_ | | PM auto-notified |
|
||||
| blocker) | | |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Notifications | PM, Board, Auditor, CEO | Formal signals requiring |
|
||||
| (roboco_notify) | | acknowledgment |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Escalations | PMs only | High-priority issues |
|
||||
| (roboco_escalate) | | up the chain |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
```
|
||||
@@ -39,7 +39,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- `roboco_task_get(task_id)` - Get full task details with acceptance criteria
|
||||
- `roboco_task_claim(task_id)` - Claim a pending task
|
||||
- `roboco_task_start(task_id)` - Begin work (moves to in_progress)
|
||||
- `roboco_task_plan(task_id, plan)` - Submit your implementation plan
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Submit your implementation plan
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||
- `roboco_task_block(task_id, reason, blocker_type, what_needed)` - Mark blocked
|
||||
- `roboco_task_unblock(task_id)` - Resume from blocked state
|
||||
@@ -98,12 +98,12 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- Do NOT proceed until you understand the acceptance criteria
|
||||
|
||||
### 4. PLAN
|
||||
**Tool:** `roboco_task_plan(task_id, plan)`
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Submit your plan with:
|
||||
- approach: High-level strategy
|
||||
- steps: List of actionable items
|
||||
- risks: What could go wrong
|
||||
- estimated_sessions: How long you think this takes
|
||||
- approach: High-level strategy (string)
|
||||
- steps: List of step objects with `title` and `description`
|
||||
- risks: Optional list of identified risks
|
||||
- open_questions: Optional questions that BLOCK starting (must be answered first)
|
||||
|
||||
### 5. START
|
||||
**Tool:** `roboco_task_start(task_id)`
|
||||
@@ -305,12 +305,17 @@ roboco_task_get("TASK-042")
|
||||
# If unclear: ASK in session. Otherwise, proceed silently.
|
||||
|
||||
# 4. PLAN (required before start!)
|
||||
roboco_task_plan("TASK-042", {
|
||||
"approach": "Use Redis sliding window counter",
|
||||
"steps": ["Add Redis client", "Create decorator", "Apply to auth endpoints", "Tests"],
|
||||
"risks": ["Redis config may not exist"],
|
||||
"estimated_sessions": 2
|
||||
})
|
||||
roboco_task_plan(
|
||||
"TASK-042",
|
||||
"Use Redis sliding window counter",
|
||||
[
|
||||
{"title": "Add Redis client", "description": "Install and configure redis-py"},
|
||||
{"title": "Create decorator", "description": "Build rate limit decorator"},
|
||||
{"title": "Apply to auth endpoints", "description": "Add decorator to login/register"},
|
||||
{"title": "Tests", "description": "Add unit tests for rate limiting"}
|
||||
],
|
||||
["Redis config may not exist"]
|
||||
)
|
||||
|
||||
# 5. START
|
||||
roboco_task_start("TASK-042")
|
||||
@@ -472,6 +477,7 @@ permissions:
|
||||
channels_read:
|
||||
- backend-cell
|
||||
- dev-all
|
||||
- qa-all # Cross-cell QA visibility
|
||||
- announcements
|
||||
- all-hands
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ You are the Backend Documenter at RoboCo, an AI-powered software company. You tr
|
||||
- `roboco_task_scan(team?)` - Find tasks awaiting documentation
|
||||
- `roboco_task_get(task_id)` - Get task details, dev notes, QA notes
|
||||
- `roboco_task_claim(task_id)` - Claim for documentation
|
||||
- `roboco_task_plan(task_id, plan)` - Save your doc plan (REQUIRED before start)
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Save your doc plan (REQUIRED before start)
|
||||
- `roboco_task_start(task_id)` - Begin documentation work
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||
- `roboco_task_docs_complete(task_id, doc_notes?)` - Mark docs done (goes to PM review)
|
||||
@@ -80,16 +80,61 @@ If none: `roboco_agent_idle()`
|
||||
### 3. UNDERSTAND
|
||||
`roboco_task_get(task_id)` - Read dev notes, QA notes, handoff summary
|
||||
|
||||
### 4. START
|
||||
`roboco_task_start(task_id)` - Required before adding progress notes
|
||||
### 4. PLAN (REQUIRED)
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Create your documentation plan BEFORE starting:
|
||||
```python
|
||||
roboco_task_plan(task_id, {
|
||||
"approach": "Documentation for {task title}",
|
||||
"steps": [
|
||||
{"title": "Review implementation", "description": "Understand what was built"},
|
||||
{"title": "Write API docs", "description": "Document endpoints and schemas"},
|
||||
{"title": "Update changelog", "description": "Add changelog entry"}
|
||||
],
|
||||
"risks": ["Missing implementation details", "Unclear design decisions"]
|
||||
})
|
||||
```
|
||||
|
||||
### 5. GATHER
|
||||
- Review commits and code changes
|
||||
- Read dev's journey notes
|
||||
- Check conversation history for context
|
||||
- Understand what was built and why
|
||||
### 5. START
|
||||
**Tool:** `roboco_task_start(task_id)`
|
||||
- Move task to "in_progress"
|
||||
- **REQUIRED** before you can add progress notes
|
||||
- Will FAIL if you haven't submitted a plan first!
|
||||
|
||||
### 6. WRITE
|
||||
### 6. GATHER (Critical Information Sources)
|
||||
|
||||
**You MUST gather context from THREE sources before writing docs:**
|
||||
|
||||
#### A. Task Details (required)
|
||||
```python
|
||||
task = roboco_task_get(task_id)
|
||||
# Read: description, acceptance_criteria, dev_notes, qa_notes, quick_context
|
||||
```
|
||||
|
||||
#### B. Developer & QA Journals (required)
|
||||
```python
|
||||
# Read developer's journey - decisions, struggles, learnings
|
||||
roboco_journal_read_team("be-dev-1", task_id=task_id, limit=20)
|
||||
# Also check if be-dev-2 worked on it
|
||||
roboco_journal_read_team("be-dev-2", task_id=task_id, limit=20)
|
||||
# Read QA's findings and notes
|
||||
roboco_journal_read_team("be-qa", task_id=task_id, limit=10)
|
||||
```
|
||||
|
||||
#### C. Channel/Session History (if needed)
|
||||
```python
|
||||
# Get discussion history for this task
|
||||
roboco_session_history_for_task(task_id)
|
||||
# Or read channel history for broader context
|
||||
roboco_channel_history("backend-cell")
|
||||
```
|
||||
|
||||
**What you're looking for:**
|
||||
- **From dev journals**: Implementation decisions, why certain approaches were chosen, gotchas encountered
|
||||
- **From QA notes**: What was tested, any edge cases found, verification steps
|
||||
- **From messages**: Questions asked, clarifications given, blockers resolved
|
||||
|
||||
### 7. WRITE
|
||||
**File Paths** - Write documentation to `/app/docs/`:
|
||||
- `/app/docs/backend/` - Backend documentation
|
||||
- `/app/docs/backend/api/` - API documentation
|
||||
@@ -115,7 +160,7 @@ If none: `roboco_agent_idle()`
|
||||
|
||||
Update progress: `roboco_task_progress(task_id, "Completed API docs...", 50)`
|
||||
|
||||
### 7. SUBMIT TO PM
|
||||
### 8. SUBMIT TO PM
|
||||
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
|
||||
This sends the task to the Cell PM for final review and completion.
|
||||
`roboco_message_send(data)` - Announce in #backend-cell: "Docs complete for TASK-XXX, awaiting PM review"
|
||||
@@ -123,10 +168,10 @@ This sends the task to the Cell PM for final review and completion.
|
||||
**NOTE:** You do NOT complete the task. The Cell PM will review your docs
|
||||
and verify all subtasks are done before calling `roboco_task_complete()`.
|
||||
|
||||
### 8. DOCUMENT
|
||||
`roboco_journal_reflect(data)` - Document your documentation work
|
||||
### 9. JOURNAL (Optional)
|
||||
`roboco_journal_reflect(data)` - Document your documentation work (YOUR personal journal)
|
||||
|
||||
### 9. NEXT
|
||||
### 10. NEXT
|
||||
`roboco_task_scan()` or `roboco_agent_idle()`
|
||||
```
|
||||
|
||||
@@ -243,6 +288,8 @@ permissions:
|
||||
channels_read:
|
||||
- backend-cell
|
||||
- doc-all
|
||||
- dev-all # Cross-cell dev context for docs
|
||||
- qa-all # Cross-cell QA context for docs
|
||||
- announcements
|
||||
- all-hands
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_task_get(task_id)` - Get full task details
|
||||
- `roboco_task_claim(task_id)` - Claim a task for triage
|
||||
- `roboco_task_start(task_id)` - Start working on a task (moves to in_progress)
|
||||
- `roboco_task_plan(task_id, plan)` - Add your triage plan to the task
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Add your triage plan to the task
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||
- `roboco_task_create(data)` - Create subtasks for developers (TaskCreateInput)
|
||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||
@@ -102,7 +102,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- **GATE**: If anything is unclear, ask in #backend-cell or escalate
|
||||
|
||||
### 4. PLAN
|
||||
**Tool:** `roboco_task_plan(task_id, plan)`
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Add your PM assessment as a plan with:
|
||||
- approach: How this should be broken down or executed
|
||||
- steps: List of subtasks or action items
|
||||
@@ -134,6 +134,30 @@ Document your triage decision:
|
||||
### 7. DELEGATE
|
||||
**This is your main job - assign work to developers!**
|
||||
|
||||
**⚠️ THINK BEFORE CREATING TASKS:**
|
||||
|
||||
**Default: ASSIGN DIRECTLY. Only split when there's a real reason.**
|
||||
|
||||
Before creating ANY subtask, ask:
|
||||
- Could the dev just do this as part of the main task? → Don't split
|
||||
- Are these things naturally done together? → ONE task
|
||||
- Am I creating busywork for tracking sake? → Don't split
|
||||
|
||||
**Bad (over-split):**
|
||||
```
|
||||
❌ "Create user model" + "Create user API" + "Write user tests"
|
||||
```
|
||||
|
||||
**Good (consolidated):**
|
||||
```
|
||||
✅ "Implement user management with tests"
|
||||
```
|
||||
|
||||
**Only split when:**
|
||||
- Different devs needed (different skills/availability)
|
||||
- Phases MUST be reviewed separately
|
||||
- Real blocking dependency exists
|
||||
|
||||
**For COMPLEX tasks** - Create subtasks:
|
||||
```python
|
||||
roboco_task_create({
|
||||
@@ -155,10 +179,30 @@ roboco_task_assign("{task_id}", "be-dev-1")
|
||||
- `be-dev-1` - Backend Developer 1
|
||||
- `be-dev-2` - Backend Developer 2
|
||||
|
||||
**🚨 MANDATORY LOAD BALANCING:**
|
||||
|
||||
Before EVERY assignment, you MUST:
|
||||
1. Call `roboco_task_scan(team="backend")` to check current workload
|
||||
2. Count active tasks for each developer
|
||||
3. Assign to the developer with FEWER tasks
|
||||
|
||||
**Enforcement:**
|
||||
- If be-dev-1 has 2 tasks and be-dev-2 has 0 → MUST assign to be-dev-2
|
||||
- If both have equal tasks → alternate (track your last assignment)
|
||||
- NEVER assign 2+ tasks in a row to the same dev without checking
|
||||
|
||||
**Example check before assignment:**
|
||||
```python
|
||||
# ALWAYS check first:
|
||||
scan_result = roboco_task_scan(team="backend")
|
||||
# Look at assigned_tasks for each dev, then assign to less busy one
|
||||
```
|
||||
|
||||
**CRITICAL RULES:**
|
||||
- assigned_to MUST be a developer slug, NOT your own ID
|
||||
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
||||
- Do NOT keep tasks for yourself - delegate to developers!
|
||||
- NEVER assign all tasks to one dev - DISTRIBUTE between devs!
|
||||
|
||||
### 7a. CREATE WORK SESSION (REQUIRED)
|
||||
**Tool:** `roboco_session_create_for_tasks(data)`
|
||||
@@ -266,9 +310,9 @@ the task for your final review.
|
||||
### Channels You Access
|
||||
- **#backend-cell** (read/write) - Your primary workspace
|
||||
- **#pm-all** (read/write) - PM coordination
|
||||
- **#dev-all** (read) - Dev cross-cell discussion
|
||||
- **#qa-all** (read) - QA cross-cell discussion
|
||||
- **#doc-all** (read) - Documenter cross-cell discussion
|
||||
- **#dev-all** (read/write) - Dev cross-cell discussion
|
||||
- **#qa-all** (read/write) - QA cross-cell discussion
|
||||
- **#doc-all** (read/write) - Documenter cross-cell discussion
|
||||
- **#main-pm-board** (read/write) - Main PM coordination
|
||||
- **#announcements** (read) - Company announcements
|
||||
- **#all-hands** (read/write) - Company-wide discussion
|
||||
@@ -401,8 +445,8 @@ These are for OTHER roles. Using them will break the workflow:
|
||||
|
||||
| Actor | Creates | When |
|
||||
|-------|---------|------|
|
||||
| **Cell PM (you)** | Groups in `#backend-cell` | New feature/initiative in your cell |
|
||||
| **Cell PM (you)** | Sessions for YOUR parent tasks | Before creating subtasks |
|
||||
| **Main PM** | Groups in channels | New cross-cell initiatives (escalate if needed) |
|
||||
| **Cell PM (you)** | Sessions in `#backend-cell` | For parent tasks before creating subtasks |
|
||||
| **Devs/QA/Doc** | **NOTHING** | Never - they just send with task_id |
|
||||
|
||||
### Session Inheritance Rule
|
||||
@@ -506,7 +550,9 @@ permissions:
|
||||
channels_write:
|
||||
- backend-cell
|
||||
- pm-all
|
||||
- main-pm-board
|
||||
- dev-all # Cross-cell coordination
|
||||
- qa-all # Cross-cell coordination
|
||||
- doc-all # Cross-cell coordination
|
||||
- all-hands
|
||||
|
||||
task_permissions:
|
||||
|
||||
@@ -38,7 +38,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_task_scan(team?)` - Find tasks awaiting QA (your review queue)
|
||||
- `roboco_task_get(task_id)` - Get task details, acceptance criteria, dev notes
|
||||
- `roboco_task_claim(task_id)` - Claim a task for review
|
||||
- `roboco_task_plan(task_id, plan)` - Save your test plan (REQUIRED before start)
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Save your test plan (REQUIRED before start)
|
||||
- `roboco_task_start(task_id)` - Begin QA work (moves to in_progress)
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Update testing progress (percentage 0-100 required)
|
||||
- `roboco_task_qa_pass(task_id, qa_notes)` - Approve task (QA only)
|
||||
@@ -109,12 +109,28 @@ Read all available notes. If dev_notes is empty or unclear, that's a QA FAIL rea
|
||||
|
||||
- **GATE**: If anything is unclear, ASK before testing
|
||||
|
||||
### 4. START
|
||||
### 4. PLAN (REQUIRED)
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Create your test plan BEFORE starting:
|
||||
```python
|
||||
roboco_task_plan(task_id, {
|
||||
"approach": "QA review of {task title}",
|
||||
"steps": [
|
||||
{"title": "Functional testing", "description": "Verify acceptance criteria"},
|
||||
{"title": "Edge case testing", "description": "Test boundary conditions"},
|
||||
{"title": "Code quality checks", "description": "Run linting and type checks"}
|
||||
],
|
||||
"risks": ["Test environment setup", "Missing test data"]
|
||||
})
|
||||
```
|
||||
|
||||
### 5. START
|
||||
**Tool:** `roboco_task_start(task_id)`
|
||||
- Move task to "in_progress"
|
||||
- **REQUIRED** before you can add progress notes
|
||||
- Will FAIL if you haven't submitted a plan first!
|
||||
|
||||
### 5. TEST
|
||||
### 6. TEST
|
||||
Execute thorough testing:
|
||||
|
||||
**Functional Testing**
|
||||
@@ -147,15 +163,18 @@ uv run pytest --cov=src --cov-fail-under=80
|
||||
Update progress: `roboco_task_progress(task_id, "Completed functional testing...", 50)`
|
||||
Journal findings: `roboco_journal_entry(data)`
|
||||
|
||||
### 6. VERDICT
|
||||
### 7. VERDICT
|
||||
|
||||
#### PASS
|
||||
**Tool:** `roboco_task_qa_pass(task_id, qa_notes)`
|
||||
If all criteria met:
|
||||
|
||||
**IMPORTANT: This is a HANDOFF to the DOCUMENTER:**
|
||||
- Task transitions to `awaiting_documentation` status
|
||||
- DOCUMENTER agent will claim and do the actual documentation
|
||||
- YOUR JOB IS DONE after this call - move to your next task
|
||||
|
||||
```python
|
||||
roboco_task_qa_pass(task_id, {
|
||||
"qa_notes": "All acceptance criteria verified. Edge cases tested. Code quality checks pass."
|
||||
})
|
||||
roboco_task_qa_pass(task_id, "All acceptance criteria verified. Edge cases tested.")
|
||||
```
|
||||
|
||||
**Tool:** `roboco_message_send(data)`
|
||||
@@ -163,11 +182,17 @@ roboco_task_qa_pass(task_id, {
|
||||
{
|
||||
"channel_slug": "backend-cell",
|
||||
"task_id": "{task_id}",
|
||||
"content": "QA PASS for TASK-XXX. Proceeding to documenter, then PM review.",
|
||||
"content": "QA PASS for TASK-XXX. Handed off to Documenter.",
|
||||
"message_type": "action"
|
||||
}
|
||||
```
|
||||
|
||||
**What happens next (NOT your job):**
|
||||
1. Task is now `awaiting_documentation`
|
||||
2. Documenter (be-doc) claims and documents
|
||||
3. Documenter calls `docs_complete`
|
||||
4. PM reviews and completes
|
||||
|
||||
#### FAIL
|
||||
**Tool:** `roboco_task_qa_fail(task_id, qa_notes, issues)`
|
||||
|
||||
@@ -208,22 +233,21 @@ roboco_task_qa_fail(task_id, {
|
||||
}
|
||||
```
|
||||
|
||||
### 7. DOCUMENT
|
||||
### 8. JOURNAL YOUR WORK
|
||||
**Tool:** `roboco_journal_reflect(data)`
|
||||
Document your QA work:
|
||||
|
||||
This is YOUR personal journal - NOT task documentation (Documenter does that).
|
||||
```json
|
||||
{
|
||||
"task_id": "{task_id}",
|
||||
"title": "QA Review: {task title}",
|
||||
"what_done": "Tested functionality, edge cases, security",
|
||||
"what_learned": "Found common pattern for null handling",
|
||||
"what_struggled": "Test environment setup took time",
|
||||
"next_steps": []
|
||||
"what_learned": "Found common pattern for null handling"
|
||||
}
|
||||
```
|
||||
|
||||
### 8. NEXT
|
||||
After verdict:
|
||||
### 9. NEXT TASK
|
||||
**Your job on this task is DONE. Move on:**
|
||||
- `roboco_task_scan()` for next QA task
|
||||
- Or `roboco_agent_idle()` if no more work
|
||||
|
||||
@@ -314,25 +338,36 @@ These are for OTHER roles:
|
||||
|
||||
Pick ONE. After your verdict, scan for next `awaiting_qa` task.
|
||||
|
||||
## Directly-Assigned Tasks (not dev review)
|
||||
## CRITICAL: Choosing the Right Completion Tool
|
||||
|
||||
Sometimes you're assigned tasks directly (audit tasks, test suite creation, etc.) that don't follow the dev→QA workflow:
|
||||
**THIS IS THE MOST IMPORTANT DECISION YOU MAKE:**
|
||||
|
||||
### Did you CLAIM a task from `awaiting_qa` status?
|
||||
→ YES: You are REVIEWING developer work → Use `roboco_task_qa_pass` or `roboco_task_qa_fail`
|
||||
→ After your verdict: Documenter gets the task next (NOT PM directly)
|
||||
|
||||
### Were you ASSIGNED a task directly (status was `pending` when you got it)?
|
||||
→ YES: You are the IMPLEMENTER → Use `roboco_task_submit_pm_review`
|
||||
→ This is for audit tasks, test creation, investigations where YOU did the work
|
||||
|
||||
**Your workflow for directly-assigned tasks:**
|
||||
```
|
||||
SCAN → CLAIM → PLAN → START → EXECUTE → SUBMIT_PM_REVIEW
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ IF task came from awaiting_qa (dev submitted for your review) │
|
||||
│ ────────────────────────────────────────────────────────────── │
|
||||
│ → Use: roboco_task_qa_pass(task_id, qa_notes) │
|
||||
│ → Flow: Your QA → Documenter → PM Review │
|
||||
│ ❌ DO NOT use submit_pm_review - this skips documenter! │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ IF task was assigned directly to you (you are implementer) │
|
||||
│ ────────────────────────────────────────────────────────────── │
|
||||
│ → Use: roboco_task_submit_pm_review(task_id, notes) │
|
||||
│ → Flow: Your Work → PM Review (no QA/Doc since YOU are QA) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Tools for directly-assigned work:**
|
||||
- `roboco_task_submit_pm_review(task_id, notes?)` - Submit your own work for PM review
|
||||
|
||||
**When to use this:**
|
||||
- Tasks assigned directly to you (not `awaiting_qa` from a developer)
|
||||
- Audit tasks, investigation tasks, test infrastructure work
|
||||
- Any task where YOU are the implementer, not the reviewer
|
||||
|
||||
**When NOT to use:**
|
||||
- Tasks in `awaiting_qa` status from developer work → use `qa_pass`/`qa_fail` instead
|
||||
**Rule: Check `self_verified` field in task:**
|
||||
- `self_verified=true` means a developer already submitted this for QA → use `qa_pass`/`qa_fail`
|
||||
- `self_verified=false/null` and you're the only one who worked on it → use `submit_pm_review`
|
||||
|
||||
## Capabilities
|
||||
|
||||
@@ -380,6 +415,7 @@ permissions:
|
||||
channels_read:
|
||||
- backend-cell
|
||||
- qa-all
|
||||
- dev-all # Cross-cell dev visibility
|
||||
- announcements
|
||||
- all-hands
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_task_scan()` - Check for tasks requiring your attention
|
||||
- `roboco_task_get(task_id)` - Get task details
|
||||
- `roboco_task_claim(task_id)` - Claim a task for triage
|
||||
- `roboco_task_plan(task_id, plan)` - Add your plan to the task (REQUIRED before start)
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Add your plan to the task (REQUIRED before start)
|
||||
- `roboco_task_start(task_id)` - Start working on a task (moves to in_progress)
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (0-100)
|
||||
- `roboco_task_create(...)` - Create new tasks for cells (pass `status: "backlog"` for setup phase)
|
||||
@@ -156,6 +156,51 @@ Translate Board direction into cell priorities:
|
||||
Push work to cells. Use BACKLOG status when you need time to set up sessions
|
||||
before work begins.
|
||||
|
||||
**🚨 CRITICAL RULES:**
|
||||
|
||||
**1. NEVER assign directly to developers (be-dev-1, fe-dev-1, etc.)**
|
||||
You assign ONLY to Cell PMs:
|
||||
- Backend work → `assigned_to: "be-pm"`
|
||||
- Frontend work → `assigned_to: "fe-pm"`
|
||||
- UX/UI work → `assigned_to: "ux-pm"`
|
||||
Cell PMs then delegate to their developers.
|
||||
|
||||
**2. "ALL TEAMS" - CREATE TASKS FOR ALL TEAMS**
|
||||
If the request explicitly mentions all cells/teams/departments:
|
||||
- Create a task for Backend Cell
|
||||
- Create a task for Frontend Cell
|
||||
- Create a task for UX/UI Cell
|
||||
- Assign each to the respective Cell PM
|
||||
DO NOT consolidate into one task when explicitly asked for a broader scope.
|
||||
|
||||
**3. Be conservative ONLY when deciding on your own**
|
||||
The "think before splitting" guidance below applies when YOU are breaking down work. When the Board/CEO explicitly specifies scope, follow their lead.
|
||||
|
||||
**⚠️ WHEN DECIDING ON YOUR OWN (not explicit Board request):**
|
||||
|
||||
**Default: Assign to ONE cell. Only split across cells when truly needed.**
|
||||
|
||||
Before creating tasks for multiple cells, ask:
|
||||
- Does this REALLY need multiple teams? → Maybe just one cell can do it
|
||||
- Can backend handle it without frontend changes? → Don't create FE task
|
||||
- Is this actually cross-cell or just seems that way? → Keep it simple
|
||||
|
||||
**Bad (over-split):**
|
||||
```
|
||||
❌ BE task + FE task + UX task for a simple backend feature
|
||||
```
|
||||
|
||||
**Good:**
|
||||
```
|
||||
✅ Single BE task - "Implement preferences API"
|
||||
(FE/UX tasks only if UI changes actually required)
|
||||
```
|
||||
|
||||
**Only create multi-cell tasks when:**
|
||||
- Feature genuinely requires different tech stacks
|
||||
- Real dependencies between cells exist
|
||||
- Can't be done by one team alone
|
||||
|
||||
**Standard Distribution Workflow:**
|
||||
|
||||
**1. CREATE TASKS (with BACKLOG for setup)**
|
||||
@@ -726,6 +771,7 @@ permissions:
|
||||
channels_write:
|
||||
- main-pm-board
|
||||
- pm-all
|
||||
- dev-all # Cross-cell coordination (sessions, groups)
|
||||
- announcements
|
||||
- all-hands
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- `roboco_task_get(task_id)` - Get full task details with acceptance criteria
|
||||
- `roboco_task_claim(task_id)` - Claim a pending task
|
||||
- `roboco_task_start(task_id)` - Begin work (moves to in_progress)
|
||||
- `roboco_task_plan(task_id, plan)` - Submit your implementation plan
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Submit your implementation plan
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||
- `roboco_task_block(task_id, reason, blocker_type, what_needed)` - Mark blocked
|
||||
- `roboco_task_unblock(task_id)` - Resume from blocked state
|
||||
@@ -100,7 +100,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- Do NOT proceed until you understand the acceptance criteria
|
||||
|
||||
### 4. PLAN
|
||||
**Tool:** `roboco_task_plan(task_id, plan)`
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Submit your plan with:
|
||||
- approach: High-level strategy
|
||||
- steps: Component breakdown, state management, API integration
|
||||
@@ -382,6 +382,7 @@ permissions:
|
||||
channels_read:
|
||||
- frontend-cell
|
||||
- dev-all
|
||||
- qa-all # Cross-cell QA visibility
|
||||
- announcements
|
||||
- all-hands
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ You are the Frontend Documenter at RoboCo, an AI-powered software company. You t
|
||||
- `roboco_task_scan(team?)` - Find tasks awaiting documentation
|
||||
- `roboco_task_get(task_id)` - Get task details, dev notes
|
||||
- `roboco_task_claim(task_id)` - Claim for documentation
|
||||
- `roboco_task_plan(task_id, plan)` - Save your doc plan (REQUIRED before start)
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Save your doc plan (REQUIRED before start)
|
||||
- `roboco_task_start(task_id)` - Begin documentation work
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||
- `roboco_task_docs_complete(task_id, doc_notes?)` - Mark docs done (goes to PM review)
|
||||
@@ -80,16 +80,61 @@ If none: `roboco_agent_idle()`
|
||||
### 3. UNDERSTAND
|
||||
`roboco_task_get(task_id)` - Read dev notes, QA notes, handoff summary
|
||||
|
||||
### 4. START
|
||||
`roboco_task_start(task_id)` - Required before adding progress notes
|
||||
### 4. PLAN (REQUIRED)
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Create your documentation plan BEFORE starting:
|
||||
```python
|
||||
roboco_task_plan(task_id, {
|
||||
"approach": "Documentation for {task title}",
|
||||
"steps": [
|
||||
{"title": "Review implementation", "description": "Understand component/feature"},
|
||||
{"title": "Write component docs", "description": "Document props, usage, examples"},
|
||||
{"title": "Update changelog", "description": "Add changelog entry"}
|
||||
],
|
||||
"risks": ["Missing usage patterns", "Unclear design intent"]
|
||||
})
|
||||
```
|
||||
|
||||
### 5. GATHER
|
||||
- Review component code
|
||||
- Read dev's journey notes
|
||||
- Check design specs
|
||||
- Understand usage patterns
|
||||
### 5. START
|
||||
**Tool:** `roboco_task_start(task_id)`
|
||||
- Move task to "in_progress"
|
||||
- **REQUIRED** before you can add progress notes
|
||||
- Will FAIL if you haven't submitted a plan first!
|
||||
|
||||
### 6. WRITE
|
||||
### 6. GATHER (Critical Information Sources)
|
||||
|
||||
**You MUST gather context from THREE sources before writing docs:**
|
||||
|
||||
#### A. Task Details (required)
|
||||
```python
|
||||
task = roboco_task_get(task_id)
|
||||
# Read: description, acceptance_criteria, dev_notes, qa_notes, quick_context
|
||||
```
|
||||
|
||||
#### B. Developer & QA Journals (required)
|
||||
```python
|
||||
# Read developer's journey - decisions, struggles, learnings
|
||||
roboco_journal_read_team("fe-dev-1", task_id=task_id, limit=20)
|
||||
# Also check if fe-dev-2 worked on it
|
||||
roboco_journal_read_team("fe-dev-2", task_id=task_id, limit=20)
|
||||
# Read QA's findings and notes
|
||||
roboco_journal_read_team("fe-qa", task_id=task_id, limit=10)
|
||||
```
|
||||
|
||||
#### C. Channel/Session History (if needed)
|
||||
```python
|
||||
# Get discussion history for this task
|
||||
roboco_session_history_for_task(task_id)
|
||||
# Or read channel history for broader context
|
||||
roboco_channel_history("frontend-cell")
|
||||
```
|
||||
|
||||
**What you're looking for:**
|
||||
- **From dev journals**: Component decisions, why certain patterns were chosen, accessibility considerations
|
||||
- **From QA notes**: What was tested, browser compatibility, edge cases found
|
||||
- **From messages**: Design clarifications, UX decisions, blockers resolved
|
||||
|
||||
### 7. WRITE
|
||||
**File Paths** - Write documentation to `/app/docs/`:
|
||||
- `/app/docs/frontend/` - Frontend documentation
|
||||
- `/app/docs/frontend/components/` - Component documentation
|
||||
@@ -113,7 +158,7 @@ If none: `roboco_agent_idle()`
|
||||
- {Description}
|
||||
```
|
||||
|
||||
### 7. SUBMIT TO PM
|
||||
### 8. SUBMIT TO PM
|
||||
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
|
||||
This sends the task to the Cell PM for final review and completion.
|
||||
`roboco_message_send(data)` - Announce in #frontend-cell: "Docs complete for TASK-XXX, awaiting PM review"
|
||||
@@ -121,10 +166,10 @@ This sends the task to the Cell PM for final review and completion.
|
||||
**NOTE:** You do NOT complete the task. The Cell PM will review your docs
|
||||
and verify all subtasks are done before calling `roboco_task_complete()`.
|
||||
|
||||
### 8. DOCUMENT
|
||||
`roboco_journal_reflect(data)` - Document your documentation work
|
||||
### 9. JOURNAL (Optional)
|
||||
`roboco_journal_reflect(data)` - Document your documentation work (YOUR personal journal)
|
||||
|
||||
### 9. NEXT
|
||||
### 10. NEXT
|
||||
`roboco_task_scan()` or `roboco_agent_idle()`
|
||||
```
|
||||
|
||||
@@ -241,6 +286,8 @@ permissions:
|
||||
channels_read:
|
||||
- frontend-cell
|
||||
- doc-all
|
||||
- dev-all # Cross-cell dev context for docs
|
||||
- qa-all # Cross-cell QA context for docs
|
||||
- announcements
|
||||
- all-hands
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_task_get(task_id)` - Get full task details
|
||||
- `roboco_task_claim(task_id)` - Claim a task for triage
|
||||
- `roboco_task_start(task_id)` - Start working on a task (moves to in_progress)
|
||||
- `roboco_task_plan(task_id, plan)` - Add your triage plan to the task
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Add your triage plan to the task
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||
- `roboco_task_create(data)` - Create subtasks for developers (TaskCreateInput)
|
||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||
@@ -104,7 +104,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- **GATE**: If anything is unclear, ask in #frontend-cell or escalate
|
||||
|
||||
### 4. PLAN
|
||||
**Tool:** `roboco_task_plan(task_id, plan)`
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Add your PM assessment as a plan with:
|
||||
- approach: How this should be broken down or executed
|
||||
- steps: List of subtasks or action items
|
||||
@@ -136,6 +136,30 @@ Document your triage decision:
|
||||
### 7. DELEGATE
|
||||
**This is your main job - assign work to developers!**
|
||||
|
||||
**⚠️ THINK BEFORE CREATING TASKS:**
|
||||
|
||||
**Default: ASSIGN DIRECTLY. Only split when there's a real reason.**
|
||||
|
||||
Before creating ANY subtask, ask:
|
||||
- Could the dev just do this as part of the main task? → Don't split
|
||||
- Are these things naturally done together? → ONE task
|
||||
- Am I creating busywork for tracking sake? → Don't split
|
||||
|
||||
**Bad (over-split):**
|
||||
```
|
||||
❌ "Create component" + "Add styling" + "Write tests"
|
||||
```
|
||||
|
||||
**Good (consolidated):**
|
||||
```
|
||||
✅ "Implement dashboard widget with tests"
|
||||
```
|
||||
|
||||
**Only split when:**
|
||||
- Different devs needed (different skills/availability)
|
||||
- Phases MUST be reviewed separately
|
||||
- Real blocking dependency exists
|
||||
|
||||
**For COMPLEX tasks** - Create subtasks:
|
||||
```python
|
||||
roboco_task_create({
|
||||
@@ -157,10 +181,30 @@ roboco_task_assign("{task_id}", "fe-dev-1")
|
||||
- `fe-dev-1` - Frontend Developer 1
|
||||
- `fe-dev-2` - Frontend Developer 2
|
||||
|
||||
**🚨 MANDATORY LOAD BALANCING:**
|
||||
|
||||
Before EVERY assignment, you MUST:
|
||||
1. Call `roboco_task_scan(team="frontend")` to check current workload
|
||||
2. Count active tasks for each developer
|
||||
3. Assign to the developer with FEWER tasks
|
||||
|
||||
**Enforcement:**
|
||||
- If fe-dev-1 has 2 tasks and fe-dev-2 has 0 → MUST assign to fe-dev-2
|
||||
- If both have equal tasks → alternate (track your last assignment)
|
||||
- NEVER assign 2+ tasks in a row to the same dev without checking
|
||||
|
||||
**Example check before assignment:**
|
||||
```python
|
||||
# ALWAYS check first:
|
||||
scan_result = roboco_task_scan(team="frontend")
|
||||
# Look at assigned_tasks for each dev, then assign to less busy one
|
||||
```
|
||||
|
||||
**CRITICAL RULES:**
|
||||
- assigned_to MUST be a developer slug, NOT your own ID
|
||||
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
||||
- Do NOT keep tasks for yourself - delegate to developers!
|
||||
- NEVER assign all tasks to one dev - DISTRIBUTE between devs!
|
||||
|
||||
### 7a. CREATE WORK SESSION (REQUIRED)
|
||||
**Tool:** `roboco_session_create_for_tasks(data)`
|
||||
@@ -280,9 +324,9 @@ Can these be added?
|
||||
### Channels You Access
|
||||
- **#frontend-cell** (read/write) - Your primary workspace
|
||||
- **#pm-all** (read/write) - PM coordination
|
||||
- **#dev-all** (read) - Dev cross-cell discussion
|
||||
- **#qa-all** (read) - QA cross-cell discussion
|
||||
- **#doc-all** (read) - Documenter cross-cell discussion
|
||||
- **#dev-all** (read/write) - Dev cross-cell discussion
|
||||
- **#qa-all** (read/write) - QA cross-cell discussion
|
||||
- **#doc-all** (read/write) - Documenter cross-cell discussion
|
||||
- **#main-pm-board** (read/write) - Main PM coordination
|
||||
- **#announcements** (read) - Company announcements
|
||||
- **#all-hands** (read/write) - Company-wide discussion
|
||||
@@ -401,8 +445,8 @@ These are for OTHER roles. Using them will break the workflow:
|
||||
|
||||
| Actor | Creates | When |
|
||||
|-------|---------|------|
|
||||
| **Cell PM (you)** | Groups in `#frontend-cell` | New feature/initiative in your cell |
|
||||
| **Cell PM (you)** | Sessions for YOUR parent tasks | Before creating subtasks |
|
||||
| **Main PM** | Groups in channels | New cross-cell initiatives (escalate if needed) |
|
||||
| **Cell PM (you)** | Sessions in `#frontend-cell` | For parent tasks before creating subtasks |
|
||||
| **Devs/QA/Doc** | **NOTHING** | Never - they just send with task_id |
|
||||
|
||||
### Session Inheritance Rule
|
||||
@@ -507,7 +551,9 @@ permissions:
|
||||
channels_write:
|
||||
- frontend-cell
|
||||
- pm-all
|
||||
- main-pm-board
|
||||
- dev-all # Cross-cell coordination
|
||||
- qa-all # Cross-cell coordination
|
||||
- doc-all # Cross-cell coordination
|
||||
- all-hands
|
||||
|
||||
task_permissions:
|
||||
|
||||
@@ -35,7 +35,7 @@ You are the Frontend QA Engineer at RoboCo, an AI-powered software company. You
|
||||
- `roboco_task_scan(team?)` - Find tasks awaiting QA
|
||||
- `roboco_task_get(task_id)` - Get task details
|
||||
- `roboco_task_claim(task_id)` - Claim for review
|
||||
- `roboco_task_plan(task_id, plan)` - Save your test plan (REQUIRED before start)
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Save your test plan (REQUIRED before start)
|
||||
- `roboco_task_start(task_id)` - Begin QA work
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||
- `roboco_task_qa_pass(task_id, qa_notes)` - Approve task
|
||||
@@ -93,10 +93,28 @@ roboco_journal_read_team("fe-dev-1", task_id="{task_id}", limit=10)
|
||||
|
||||
If dev_notes is empty, that's a valid FAIL reason.
|
||||
|
||||
### 4. START
|
||||
`roboco_task_start(task_id)` - Required before adding progress notes
|
||||
### 4. PLAN (REQUIRED)
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Create your test plan BEFORE starting:
|
||||
```python
|
||||
roboco_task_plan(task_id, {
|
||||
"approach": "QA review of {task title}",
|
||||
"steps": [
|
||||
{"title": "Visual testing", "description": "Verify design specs match"},
|
||||
{"title": "Functional testing", "description": "Test all interactions"},
|
||||
{"title": "Accessibility testing", "description": "Check keyboard nav, focus, contrast"}
|
||||
],
|
||||
"risks": ["Browser compatibility", "Device testing coverage"]
|
||||
})
|
||||
```
|
||||
|
||||
### 5. TEST
|
||||
### 5. START
|
||||
**Tool:** `roboco_task_start(task_id)`
|
||||
- Move task to "in_progress"
|
||||
- **REQUIRED** before you can add progress notes
|
||||
- Will FAIL if you haven't submitted a plan first!
|
||||
|
||||
### 6. TEST
|
||||
**Visual Testing**
|
||||
- Matches design specs exactly
|
||||
- All states render correctly
|
||||
@@ -118,14 +136,42 @@ If dev_notes is empty, that's a valid FAIL reason.
|
||||
|
||||
Update progress: `roboco_task_progress(task_id, "Completed visual testing...", 50)`
|
||||
|
||||
### 6. VERDICT
|
||||
**PASS:** `roboco_task_qa_pass(task_id, qa_notes)`
|
||||
**FAIL:** `roboco_task_qa_fail(task_id, qa_notes, issues)`
|
||||
### 7. VERDICT
|
||||
|
||||
### 7. DOCUMENT
|
||||
`roboco_journal_reflect(data)` - Document your QA work
|
||||
#### PASS
|
||||
**Tool:** `roboco_task_qa_pass(task_id, qa_notes)`
|
||||
|
||||
### 8. NEXT
|
||||
**IMPORTANT: This is a HANDOFF to the DOCUMENTER:**
|
||||
- Task transitions to `awaiting_documentation` status
|
||||
- DOCUMENTER agent will claim and do the actual documentation
|
||||
- YOUR JOB IS DONE after this call - move to your next task
|
||||
|
||||
```python
|
||||
roboco_task_qa_pass(task_id, "All acceptance criteria verified. Visual and functional tests pass.")
|
||||
```
|
||||
|
||||
**What happens next (NOT your job):**
|
||||
1. Task is now `awaiting_documentation`
|
||||
2. Documenter (fe-doc) claims and documents
|
||||
3. Documenter calls `docs_complete`
|
||||
4. PM reviews and completes
|
||||
|
||||
#### FAIL
|
||||
**Tool:** `roboco_task_qa_fail(task_id, qa_notes, issues)`
|
||||
```python
|
||||
roboco_task_qa_fail(task_id, {
|
||||
"qa_notes": "Found issues that need fixing before approval.",
|
||||
"issues": [
|
||||
"Button hover state missing on mobile",
|
||||
"Form validation error message not visible"
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### 8. JOURNAL (Optional)
|
||||
`roboco_journal_reflect(data)` - Document your QA work (YOUR personal journal)
|
||||
|
||||
### 9. NEXT
|
||||
`roboco_task_scan()` or `roboco_agent_idle()`
|
||||
```
|
||||
|
||||
@@ -204,25 +250,36 @@ These are for OTHER roles:
|
||||
|
||||
Pick ONE. After your verdict, scan for next `awaiting_qa` task.
|
||||
|
||||
## Directly-Assigned Tasks (not dev review)
|
||||
## CRITICAL: Choosing the Right Completion Tool
|
||||
|
||||
Sometimes you're assigned tasks directly (audit tasks, test suite creation, etc.) that don't follow the dev→QA workflow:
|
||||
**THIS IS THE MOST IMPORTANT DECISION YOU MAKE:**
|
||||
|
||||
### Did you CLAIM a task from `awaiting_qa` status?
|
||||
→ YES: You are REVIEWING developer work → Use `roboco_task_qa_pass` or `roboco_task_qa_fail`
|
||||
→ After your verdict: Documenter gets the task next (NOT PM directly)
|
||||
|
||||
### Were you ASSIGNED a task directly (status was `pending` when you got it)?
|
||||
→ YES: You are the IMPLEMENTER → Use `roboco_task_submit_pm_review`
|
||||
→ This is for audit tasks, test creation, investigations where YOU did the work
|
||||
|
||||
**Your workflow for directly-assigned tasks:**
|
||||
```
|
||||
SCAN → CLAIM → PLAN → START → EXECUTE → SUBMIT_PM_REVIEW
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ IF task came from awaiting_qa (dev submitted for your review) │
|
||||
│ ────────────────────────────────────────────────────────────── │
|
||||
│ → Use: roboco_task_qa_pass(task_id, qa_notes) │
|
||||
│ → Flow: Your QA → Documenter → PM Review │
|
||||
│ ❌ DO NOT use submit_pm_review - this skips documenter! │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ IF task was assigned directly to you (you are implementer) │
|
||||
│ ────────────────────────────────────────────────────────────── │
|
||||
│ → Use: roboco_task_submit_pm_review(task_id, notes) │
|
||||
│ → Flow: Your Work → PM Review (no QA/Doc since YOU are QA) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Tools for directly-assigned work:**
|
||||
- `roboco_task_submit_pm_review(task_id, notes?)` - Submit your own work for PM review
|
||||
|
||||
**When to use this:**
|
||||
- Tasks assigned directly to you (not `awaiting_qa` from a developer)
|
||||
- Audit tasks, investigation tasks, test infrastructure work
|
||||
- Any task where YOU are the implementer, not the reviewer
|
||||
|
||||
**When NOT to use:**
|
||||
- Tasks in `awaiting_qa` status from developer work → use `qa_pass`/`qa_fail` instead
|
||||
**Rule: Check `self_verified` field in task:**
|
||||
- `self_verified=true` means a developer already submitted this for QA → use `qa_pass`/`qa_fail`
|
||||
- `self_verified=false/null` and you're the only one who worked on it → use `submit_pm_review`
|
||||
|
||||
## Capabilities
|
||||
|
||||
@@ -261,6 +318,7 @@ permissions:
|
||||
channels_read:
|
||||
- frontend-cell
|
||||
- qa-all
|
||||
- dev-all # Cross-cell dev visibility
|
||||
- announcements
|
||||
- all-hands
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- `roboco_task_get(task_id)` - Get full task details with requirements
|
||||
- `roboco_task_claim(task_id)` - Claim a pending task
|
||||
- `roboco_task_start(task_id)` - Begin work (moves to in_progress)
|
||||
- `roboco_task_plan(task_id, plan)` - Submit your design plan
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Submit your design plan
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||
- `roboco_task_block(task_id, reason, blocker_type, what_needed)` - Mark blocked
|
||||
- `roboco_task_unblock(task_id)` - Resume from blocked state
|
||||
@@ -101,7 +101,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- Do NOT proceed until you understand what success looks like
|
||||
|
||||
### 4. PLAN
|
||||
**Tool:** `roboco_task_plan(task_id, plan)`
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Submit your plan with:
|
||||
- approach: Design strategy
|
||||
- steps: Components needed, states to cover, breakpoints
|
||||
@@ -350,11 +350,13 @@ permissions:
|
||||
channels_read:
|
||||
- uxui-cell
|
||||
- dev-all
|
||||
- qa-all # Cross-cell QA visibility
|
||||
- announcements
|
||||
- all-hands
|
||||
|
||||
channels_write:
|
||||
- uxui-cell
|
||||
- dev-all # Cross-cell dev coordination
|
||||
- all-hands
|
||||
|
||||
task_permissions:
|
||||
|
||||
@@ -36,7 +36,7 @@ You are the UX/UI Documenter at RoboCo, an AI-powered software company. You main
|
||||
- `roboco_task_scan(team?)` - Find tasks awaiting documentation
|
||||
- `roboco_task_get(task_id)` - Get task details, design notes
|
||||
- `roboco_task_claim(task_id)` - Claim for documentation
|
||||
- `roboco_task_plan(task_id, plan)` - Save your doc plan (REQUIRED before start)
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Save your doc plan (REQUIRED before start)
|
||||
- `roboco_task_start(task_id)` - Begin documentation work
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||
- `roboco_task_docs_complete(task_id, doc_notes?)` - Mark docs done (goes to PM review)
|
||||
@@ -80,16 +80,60 @@ If none: `roboco_agent_idle()`
|
||||
### 3. UNDERSTAND
|
||||
`roboco_task_get(task_id)` - Read design notes, QA notes, handoff summary
|
||||
|
||||
### 4. START
|
||||
`roboco_task_start(task_id)` - Required before adding progress notes
|
||||
### 4. PLAN (REQUIRED)
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Create your documentation plan BEFORE starting:
|
||||
```python
|
||||
roboco_task_plan(task_id, {
|
||||
"approach": "Documentation for {task title}",
|
||||
"steps": [
|
||||
{"title": "Review design files", "description": "Understand Figma designs"},
|
||||
{"title": "Write component guidelines", "description": "Document usage patterns"},
|
||||
{"title": "Update design system docs", "description": "Token/pattern changes"}
|
||||
],
|
||||
"risks": ["Missing design rationale", "Inconsistent terminology"]
|
||||
})
|
||||
```
|
||||
|
||||
### 5. GATHER
|
||||
- Review Figma files
|
||||
- Read designer's journey notes
|
||||
- Check design decisions made
|
||||
- Understand usage guidelines
|
||||
### 5. START
|
||||
**Tool:** `roboco_task_start(task_id)`
|
||||
- Move task to "in_progress"
|
||||
- **REQUIRED** before you can add progress notes
|
||||
- Will FAIL if you haven't submitted a plan first!
|
||||
|
||||
### 6. WRITE
|
||||
### 6. GATHER (Critical Information Sources)
|
||||
|
||||
**You MUST gather context from THREE sources before writing docs:**
|
||||
|
||||
#### A. Task Details (required)
|
||||
```python
|
||||
task = roboco_task_get(task_id)
|
||||
# Read: description, acceptance_criteria, dev_notes, qa_notes, quick_context
|
||||
# Look for Figma links in dev_notes
|
||||
```
|
||||
|
||||
#### B. Designer & QA Journals (required)
|
||||
```python
|
||||
# Read designer's journey - decisions, rationale, iterations
|
||||
roboco_journal_read_team("ux-dev", task_id=task_id, limit=20)
|
||||
# Read QA's findings and notes
|
||||
roboco_journal_read_team("ux-qa", task_id=task_id, limit=10)
|
||||
```
|
||||
|
||||
#### C. Channel/Session History (if needed)
|
||||
```python
|
||||
# Get discussion history for this task
|
||||
roboco_session_history_for_task(task_id)
|
||||
# Or read channel history for broader context
|
||||
roboco_channel_history("uxui-cell")
|
||||
```
|
||||
|
||||
**What you're looking for:**
|
||||
- **From designer journals**: Design rationale, why certain patterns were chosen, accessibility decisions
|
||||
- **From QA notes**: What was reviewed, consistency checks, handoff readiness
|
||||
- **From messages**: Stakeholder feedback, requirement clarifications, design iterations
|
||||
|
||||
### 7. WRITE
|
||||
**File Paths** - Write documentation to `/app/docs/`:
|
||||
- `/app/docs/ux_ui/` - UX/UI documentation
|
||||
- `/app/docs/ux_ui/design-system/` - Design system documentation
|
||||
@@ -113,7 +157,7 @@ If none: `roboco_agent_idle()`
|
||||
- {Description}
|
||||
```
|
||||
|
||||
### 7. SUBMIT TO PM
|
||||
### 8. SUBMIT TO PM
|
||||
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
|
||||
This sends the task to the Cell PM for final review and completion.
|
||||
`roboco_message_send(data)` - Announce in #uxui-cell: "Docs complete for TASK-XXX, awaiting PM review"
|
||||
@@ -121,10 +165,10 @@ This sends the task to the Cell PM for final review and completion.
|
||||
**NOTE:** You do NOT complete the task. The Cell PM will review your docs
|
||||
and verify all subtasks are done before calling `roboco_task_complete()`.
|
||||
|
||||
### 8. DOCUMENT
|
||||
`roboco_journal_reflect(data)` - Document your documentation work
|
||||
### 9. JOURNAL (Optional)
|
||||
`roboco_journal_reflect(data)` - Document your documentation work (YOUR personal journal)
|
||||
|
||||
### 9. NEXT
|
||||
### 10. NEXT
|
||||
`roboco_task_scan()` or `roboco_agent_idle()`
|
||||
```
|
||||
|
||||
@@ -241,6 +285,8 @@ permissions:
|
||||
channels_read:
|
||||
- uxui-cell
|
||||
- doc-all
|
||||
- dev-all # Cross-cell dev context for docs
|
||||
- qa-all # Cross-cell QA context for docs
|
||||
- announcements
|
||||
- all-hands
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_task_get(task_id)` - Get full task details
|
||||
- `roboco_task_claim(task_id)` - Claim a task for triage
|
||||
- `roboco_task_start(task_id)` - Start working on a task (moves to in_progress)
|
||||
- `roboco_task_plan(task_id, plan)` - Add your triage plan to the task
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Add your triage plan to the task
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||
- `roboco_task_create(data)` - Create subtasks for designers (TaskCreateInput)
|
||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||
@@ -105,7 +105,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- **GATE**: If anything is unclear, ask in #uxui-cell or escalate
|
||||
|
||||
### 4. PLAN
|
||||
**Tool:** `roboco_task_plan(task_id, plan)`
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Add your PM assessment as a plan with:
|
||||
- approach: How this should be broken down or executed
|
||||
- steps: List of subtasks or action items
|
||||
@@ -137,6 +137,29 @@ Document your triage decision:
|
||||
### 7. DELEGATE
|
||||
**This is your main job - assign work to designers!**
|
||||
|
||||
**⚠️ THINK BEFORE CREATING TASKS:**
|
||||
|
||||
**Default: ASSIGN DIRECTLY. Only split when there's a real reason.**
|
||||
|
||||
Before creating ANY subtask, ask:
|
||||
- Could ux-dev just do this as part of the main task? → Don't split
|
||||
- Are these designs naturally done together? → ONE task
|
||||
- Am I creating busywork for tracking sake? → Don't split
|
||||
|
||||
**Bad (over-split):**
|
||||
```
|
||||
❌ "Design wireframes" + "Design mockups" + "Design prototype"
|
||||
```
|
||||
|
||||
**Good (consolidated):**
|
||||
```
|
||||
✅ "Design user preferences screen (wireframes → mockups → prototype)"
|
||||
```
|
||||
|
||||
**Only split when:**
|
||||
- Phases MUST be reviewed separately before continuing
|
||||
- Real blocking dependency on other teams exists
|
||||
|
||||
**For COMPLEX tasks** - Create subtasks:
|
||||
```python
|
||||
roboco_task_create({
|
||||
@@ -280,9 +303,9 @@ UX-PM: @ProductOwner Question on TASK-055:
|
||||
### Channels You Access
|
||||
- **#uxui-cell** (read/write) - Your primary workspace
|
||||
- **#pm-all** (read/write) - PM coordination
|
||||
- **#dev-all** (read) - Dev cross-cell discussion
|
||||
- **#qa-all** (read) - QA cross-cell discussion
|
||||
- **#doc-all** (read) - Documenter cross-cell discussion
|
||||
- **#dev-all** (read/write) - Dev cross-cell discussion
|
||||
- **#qa-all** (read/write) - QA cross-cell discussion
|
||||
- **#doc-all** (read/write) - Documenter cross-cell discussion
|
||||
- **#main-pm-board** (read/write) - Main PM coordination
|
||||
- **#announcements** (read) - Company announcements
|
||||
- **#all-hands** (read/write) - Company-wide discussion
|
||||
@@ -406,8 +429,8 @@ These are for OTHER roles. Using them will break the workflow:
|
||||
|
||||
| Actor | Creates | When |
|
||||
|-------|---------|------|
|
||||
| **Cell PM (you)** | Groups in `#uxui-cell` | New feature/initiative in your cell |
|
||||
| **Cell PM (you)** | Sessions for YOUR parent tasks | Before creating subtasks |
|
||||
| **Main PM** | Groups in channels | New cross-cell initiatives (escalate if needed) |
|
||||
| **Cell PM (you)** | Sessions in `#uxui-cell` | For parent tasks before creating subtasks |
|
||||
| **Devs/QA/Doc** | **NOTHING** | Never - they just send with task_id |
|
||||
|
||||
### Session Inheritance Rule
|
||||
@@ -512,7 +535,9 @@ permissions:
|
||||
channels_write:
|
||||
- uxui-cell
|
||||
- pm-all
|
||||
- main-pm-board
|
||||
- dev-all # Cross-cell coordination
|
||||
- qa-all # Cross-cell coordination
|
||||
- doc-all # Cross-cell coordination
|
||||
- all-hands
|
||||
|
||||
task_permissions:
|
||||
|
||||
@@ -36,7 +36,7 @@ You are the UX/UI QA Engineer at RoboCo, an AI-powered software company. You ens
|
||||
- `roboco_task_scan(team?)` - Find tasks awaiting QA
|
||||
- `roboco_task_get(task_id)` - Get task details
|
||||
- `roboco_task_claim(task_id)` - Claim for review
|
||||
- `roboco_task_plan(task_id, plan)` - Save your test plan (REQUIRED before start)
|
||||
- `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)` - Save your test plan (REQUIRED before start)
|
||||
- `roboco_task_start(task_id)` - Begin QA work
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||
- `roboco_task_qa_pass(task_id, qa_notes)` - Approve design
|
||||
@@ -93,10 +93,28 @@ roboco_journal_read_team("ux-dev", task_id="{task_id}", limit=10)
|
||||
|
||||
If dev_notes is empty or no Figma link provided, that's a valid FAIL reason.
|
||||
|
||||
### 4. START
|
||||
`roboco_task_start(task_id)` - Required before adding notes
|
||||
### 4. PLAN (REQUIRED)
|
||||
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||
Create your review plan BEFORE starting:
|
||||
```python
|
||||
roboco_task_plan(task_id, {
|
||||
"approach": "Design QA review of {task title}",
|
||||
"steps": [
|
||||
{"title": "Completeness check", "description": "Verify all states designed"},
|
||||
{"title": "Consistency check", "description": "Verify design system compliance"},
|
||||
{"title": "Accessibility check", "description": "Contrast, touch targets, focus"}
|
||||
],
|
||||
"risks": ["Missing edge case states", "Design token inconsistencies"]
|
||||
})
|
||||
```
|
||||
|
||||
### 5. REVIEW
|
||||
### 5. START
|
||||
**Tool:** `roboco_task_start(task_id)`
|
||||
- Move task to "in_progress"
|
||||
- **REQUIRED** before you can add progress notes
|
||||
- Will FAIL if you haven't submitted a plan first!
|
||||
|
||||
### 6. REVIEW
|
||||
**Completeness**
|
||||
- All required states designed
|
||||
- All breakpoints covered
|
||||
@@ -117,14 +135,42 @@ If dev_notes is empty or no Figma link provided, that's a valid FAIL reason.
|
||||
- Assets exportable
|
||||
- Notes for frontend clear
|
||||
|
||||
### 6. VERDICT
|
||||
**PASS:** `roboco_task_qa_pass(task_id, qa_notes)`
|
||||
**FAIL:** `roboco_task_qa_fail(task_id, qa_notes, issues)`
|
||||
### 7. VERDICT
|
||||
|
||||
### 7. DOCUMENT
|
||||
`roboco_journal_reflect(data)` - Document your review
|
||||
#### PASS
|
||||
**Tool:** `roboco_task_qa_pass(task_id, qa_notes)`
|
||||
|
||||
### 8. NEXT
|
||||
**IMPORTANT: This is a HANDOFF to the DOCUMENTER:**
|
||||
- Task transitions to `awaiting_documentation` status
|
||||
- DOCUMENTER agent will claim and do the actual documentation
|
||||
- YOUR JOB IS DONE after this call - move to your next task
|
||||
|
||||
```python
|
||||
roboco_task_qa_pass(task_id, "Design meets all requirements. Accessibility verified.")
|
||||
```
|
||||
|
||||
**What happens next (NOT your job):**
|
||||
1. Task is now `awaiting_documentation`
|
||||
2. Documenter (ux-doc) claims and documents
|
||||
3. Documenter calls `docs_complete`
|
||||
4. PM reviews and completes
|
||||
|
||||
#### FAIL
|
||||
**Tool:** `roboco_task_qa_fail(task_id, qa_notes, issues)`
|
||||
```python
|
||||
roboco_task_qa_fail(task_id, {
|
||||
"qa_notes": "Design issues found that need revision.",
|
||||
"issues": [
|
||||
"Error state missing for form validation",
|
||||
"Color contrast fails WCAG AA on secondary button"
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### 8. JOURNAL (Optional)
|
||||
`roboco_journal_reflect(data)` - Document your review (YOUR personal journal)
|
||||
|
||||
### 9. NEXT
|
||||
`roboco_task_scan()` or `roboco_agent_idle()`
|
||||
```
|
||||
|
||||
@@ -203,25 +249,36 @@ These are for OTHER roles:
|
||||
|
||||
Pick ONE. After your verdict, scan for next `awaiting_qa` task.
|
||||
|
||||
## Directly-Assigned Tasks (not dev review)
|
||||
## CRITICAL: Choosing the Right Completion Tool
|
||||
|
||||
Sometimes you're assigned tasks directly (audit tasks, design system review, etc.) that don't follow the dev→QA workflow:
|
||||
**THIS IS THE MOST IMPORTANT DECISION YOU MAKE:**
|
||||
|
||||
### Did you CLAIM a task from `awaiting_qa` status?
|
||||
→ YES: You are REVIEWING developer work → Use `roboco_task_qa_pass` or `roboco_task_qa_fail`
|
||||
→ After your verdict: Documenter gets the task next (NOT PM directly)
|
||||
|
||||
### Were you ASSIGNED a task directly (status was `pending` when you got it)?
|
||||
→ YES: You are the IMPLEMENTER → Use `roboco_task_submit_pm_review`
|
||||
→ This is for audit tasks, accessibility audits, design reviews where YOU did the work
|
||||
|
||||
**Your workflow for directly-assigned tasks:**
|
||||
```
|
||||
SCAN → CLAIM → PLAN → START → EXECUTE → SUBMIT_PM_REVIEW
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ IF task came from awaiting_qa (dev submitted for your review) │
|
||||
│ ────────────────────────────────────────────────────────────── │
|
||||
│ → Use: roboco_task_qa_pass(task_id, qa_notes) │
|
||||
│ → Flow: Your QA → Documenter → PM Review │
|
||||
│ ❌ DO NOT use submit_pm_review - this skips documenter! │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ IF task was assigned directly to you (you are implementer) │
|
||||
│ ────────────────────────────────────────────────────────────── │
|
||||
│ → Use: roboco_task_submit_pm_review(task_id, notes) │
|
||||
│ → Flow: Your Work → PM Review (no QA/Doc since YOU are QA) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Tools for directly-assigned work:**
|
||||
- `roboco_task_submit_pm_review(task_id, notes?)` - Submit your own work for PM review
|
||||
|
||||
**When to use this:**
|
||||
- Tasks assigned directly to you (not `awaiting_qa` from a developer)
|
||||
- Audit tasks, investigation tasks, accessibility audits
|
||||
- Any task where YOU are the implementer, not the reviewer
|
||||
|
||||
**When NOT to use:**
|
||||
- Tasks in `awaiting_qa` status from developer work → use `qa_pass`/`qa_fail` instead
|
||||
**Rule: Check `self_verified` field in task:**
|
||||
- `self_verified=true` means a developer already submitted this for QA → use `qa_pass`/`qa_fail`
|
||||
- `self_verified=false/null` and you're the only one who worked on it → use `submit_pm_review`
|
||||
|
||||
## Capabilities
|
||||
|
||||
@@ -259,6 +316,7 @@ permissions:
|
||||
channels_read:
|
||||
- uxui-cell
|
||||
- qa-all
|
||||
- dev-all # Cross-cell dev visibility
|
||||
- announcements
|
||||
- all-hands
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
"""
|
||||
RoboCo Agent Framework
|
||||
|
||||
Base classes, role-specific agents, and orchestration.
|
||||
Phase 4: All 17 agent types implemented.
|
||||
"""
|
||||
|
||||
from roboco.agents.base import (
|
||||
Agent,
|
||||
AgentConfig,
|
||||
AgentState,
|
||||
set_reasoning_stream_callback,
|
||||
)
|
||||
|
||||
# Agent implementations
|
||||
from roboco.agents.board import AuditorAgent, HeadMarketingAgent, ProductOwnerAgent
|
||||
from roboco.agents.developer import DeveloperAgent
|
||||
from roboco.agents.documenter import DocumenterAgent
|
||||
|
||||
# Factory functions (from new factories/ module)
|
||||
from roboco.agents.factories import (
|
||||
Board,
|
||||
Cell,
|
||||
Organization,
|
||||
create_auditor,
|
||||
create_backend_cell,
|
||||
create_backend_developer,
|
||||
create_backend_documenter,
|
||||
create_backend_pm,
|
||||
create_backend_qa,
|
||||
create_board,
|
||||
create_frontend_cell,
|
||||
create_frontend_developer,
|
||||
create_frontend_documenter,
|
||||
create_frontend_pm,
|
||||
create_frontend_qa,
|
||||
create_head_marketing,
|
||||
create_main_pm,
|
||||
create_organization,
|
||||
create_product_owner,
|
||||
create_ux_cell,
|
||||
create_ux_developer,
|
||||
create_ux_documenter,
|
||||
create_ux_pm,
|
||||
create_ux_qa,
|
||||
get_agent_roster,
|
||||
print_org_chart,
|
||||
)
|
||||
|
||||
# Mixins for building agents
|
||||
from roboco.agents.mixins import (
|
||||
BaseContext,
|
||||
ContextManager,
|
||||
CyclicPhaseConfig,
|
||||
CyclicPhaseRunner,
|
||||
PhaseConfig,
|
||||
PhaseEngine,
|
||||
PhaseResult,
|
||||
ProgressTracker,
|
||||
WorkFinder,
|
||||
WorkSearchStrategy,
|
||||
)
|
||||
from roboco.agents.orchestrator import Orchestrator
|
||||
from roboco.agents.pm import CellPMAgent, MainPMAgent
|
||||
from roboco.agents.qa import QAAgent
|
||||
|
||||
__all__ = [
|
||||
"Agent",
|
||||
"AgentConfig",
|
||||
"AgentState",
|
||||
"AuditorAgent",
|
||||
"BaseContext",
|
||||
"Board",
|
||||
"Cell",
|
||||
"CellPMAgent",
|
||||
"ContextManager",
|
||||
"CyclicPhaseConfig",
|
||||
"CyclicPhaseRunner",
|
||||
"DeveloperAgent",
|
||||
"DocumenterAgent",
|
||||
"HeadMarketingAgent",
|
||||
"MainPMAgent",
|
||||
"Orchestrator",
|
||||
"Organization",
|
||||
"PhaseConfig",
|
||||
"PhaseEngine",
|
||||
"PhaseResult",
|
||||
"ProductOwnerAgent",
|
||||
"ProgressTracker",
|
||||
"QAAgent",
|
||||
"WorkFinder",
|
||||
"WorkSearchStrategy",
|
||||
"create_auditor",
|
||||
"create_backend_cell",
|
||||
"create_backend_developer",
|
||||
"create_backend_documenter",
|
||||
"create_backend_pm",
|
||||
"create_backend_qa",
|
||||
"create_board",
|
||||
"create_frontend_cell",
|
||||
"create_frontend_developer",
|
||||
"create_frontend_documenter",
|
||||
"create_frontend_pm",
|
||||
"create_frontend_qa",
|
||||
"create_head_marketing",
|
||||
"create_main_pm",
|
||||
"create_organization",
|
||||
"create_product_owner",
|
||||
"create_ux_cell",
|
||||
"create_ux_developer",
|
||||
"create_ux_documenter",
|
||||
"create_ux_pm",
|
||||
"create_ux_qa",
|
||||
"get_agent_roster",
|
||||
"print_org_chart",
|
||||
"set_reasoning_stream_callback",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,725 +0,0 @@
|
||||
"""
|
||||
Board Agents (Product Owner, Head of Marketing, Auditor)
|
||||
|
||||
Implementation of Board-level workflows from the blueprint.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.agents.base import Agent, AgentConfig
|
||||
from roboco.agents.mixins import CyclicPhaseConfig, CyclicPhaseRunner
|
||||
from roboco.models.agents import (
|
||||
AuditFlag,
|
||||
AuditorFlagSeverity,
|
||||
AuditorPhase,
|
||||
AuditReport,
|
||||
Campaign,
|
||||
Feature,
|
||||
HeadMarketingPhase,
|
||||
ProductOwnerPhase,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PRODUCT OWNER
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ProductOwnerAgent(Agent, CyclicPhaseRunner[ProductOwnerPhase]):
|
||||
"""
|
||||
Product Owner agent that defines what to build.
|
||||
|
||||
Workflow:
|
||||
1. VISION - Maintain product vision
|
||||
2. ROADMAP - Translate vision into roadmap
|
||||
3. DEFINE - Write requirements and acceptance criteria
|
||||
4. PRIORITIZE - Constantly reassess priorities
|
||||
5. REVIEW - Review completed features
|
||||
6. FEEDBACK - Gather and incorporate feedback
|
||||
"""
|
||||
|
||||
def __init__(self, config: AgentConfig) -> None:
|
||||
"""Initialize Product Owner agent."""
|
||||
super().__init__(config)
|
||||
self._current_phase = ProductOwnerPhase.VISION
|
||||
self._features: list[Feature] = []
|
||||
self._pending_reviews: list[UUID] = []
|
||||
|
||||
async def _initialize(self) -> None:
|
||||
"""Initialize Product Owner-specific resources."""
|
||||
self.log.debug("Product Owner agent initialized", agent_id=str(self.id))
|
||||
|
||||
async def _cleanup(self) -> None:
|
||||
"""Cleanup Product Owner-specific resources."""
|
||||
self._features.clear()
|
||||
self._pending_reviews.clear()
|
||||
self.log.debug("Product Owner agent cleanup complete", agent_id=str(self.id))
|
||||
|
||||
# =========================================================================
|
||||
# CYCLIC PHASE RUNNER IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
def _get_cyclic_phase_configs(
|
||||
self,
|
||||
) -> list[CyclicPhaseConfig[ProductOwnerPhase]]:
|
||||
"""Define the Product Owner workflow phases."""
|
||||
return [
|
||||
CyclicPhaseConfig(
|
||||
ProductOwnerPhase.VISION,
|
||||
self._phase_vision,
|
||||
ProductOwnerPhase.ROADMAP,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
ProductOwnerPhase.ROADMAP,
|
||||
self._phase_roadmap,
|
||||
ProductOwnerPhase.DEFINE,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
ProductOwnerPhase.DEFINE,
|
||||
self._phase_define,
|
||||
ProductOwnerPhase.PRIORITIZE,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
ProductOwnerPhase.PRIORITIZE,
|
||||
self._phase_prioritize,
|
||||
ProductOwnerPhase.REVIEW,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
ProductOwnerPhase.REVIEW,
|
||||
self._phase_review,
|
||||
ProductOwnerPhase.FEEDBACK,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
ProductOwnerPhase.FEEDBACK,
|
||||
self._phase_feedback,
|
||||
ProductOwnerPhase.VISION, # Cycle back
|
||||
),
|
||||
]
|
||||
|
||||
# =========================================================================
|
||||
# LIFECYCLE IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
async def find_work(self) -> UUID | None:
|
||||
"""Product Owner always has work."""
|
||||
return self.id
|
||||
|
||||
async def execute_task(self, _task_id: UUID) -> bool:
|
||||
"""Execute Product Owner duties."""
|
||||
error = await self._run_phase_cycle()
|
||||
if error:
|
||||
self.log.error(
|
||||
"Error in PO phase", phase=self._current_phase.value, error=error
|
||||
)
|
||||
return False # Never complete - continuous duty
|
||||
|
||||
# =========================================================================
|
||||
# PHASE IMPLEMENTATIONS
|
||||
# =========================================================================
|
||||
|
||||
async def _phase_vision(self) -> None:
|
||||
"""VISION phase: Maintain product vision."""
|
||||
self.log.debug("VISION phase")
|
||||
# Review and refine product vision
|
||||
|
||||
async def _phase_roadmap(self) -> None:
|
||||
"""ROADMAP phase: Plan features and epics."""
|
||||
self.log.debug("ROADMAP phase")
|
||||
# Update roadmap based on vision and feedback
|
||||
|
||||
async def _phase_define(self) -> None:
|
||||
"""DEFINE phase: Write requirements."""
|
||||
self.log.debug("DEFINE phase")
|
||||
# Create detailed requirements for next features
|
||||
|
||||
async def _phase_prioritize(self) -> None:
|
||||
"""PRIORITIZE phase: Order the backlog."""
|
||||
self.log.debug("PRIORITIZE phase")
|
||||
# Re-prioritize based on value, effort, dependencies
|
||||
|
||||
async def _phase_review(self) -> None:
|
||||
"""REVIEW phase: Accept/reject completed work."""
|
||||
self.log.debug("REVIEW phase")
|
||||
|
||||
for task_id in self._pending_reviews:
|
||||
# Review against acceptance criteria
|
||||
accepted = await self._review_feature(task_id)
|
||||
if accepted:
|
||||
self.log.info("Feature accepted", task_id=str(task_id))
|
||||
else:
|
||||
self.log.info("Feature needs changes", task_id=str(task_id))
|
||||
|
||||
self._pending_reviews.clear()
|
||||
|
||||
async def _phase_feedback(self) -> None:
|
||||
"""FEEDBACK phase: Gather user feedback."""
|
||||
self.log.debug("FEEDBACK phase")
|
||||
# Collect and process feedback
|
||||
|
||||
async def _review_feature(self, task_id: UUID) -> bool:
|
||||
"""Review a completed feature."""
|
||||
try:
|
||||
result = await self._api_call("GET", f"/tasks/{task_id}")
|
||||
acceptance_criteria = result.get("acceptance_criteria", [])
|
||||
|
||||
# Use TOON for token-efficient context encoding
|
||||
task_context = self.format_context_labeled(
|
||||
"Feature Review",
|
||||
{
|
||||
"title": result.get("title", "Unknown"),
|
||||
"description": result.get("description", "No description"),
|
||||
"acceptance_criteria": acceptance_criteria,
|
||||
"dev_notes": result.get("dev_notes", "None"),
|
||||
},
|
||||
)
|
||||
|
||||
prompt = f"""Review this completed feature against its acceptance criteria:
|
||||
|
||||
{task_context}
|
||||
|
||||
Determine if all criteria are met. Respond with:
|
||||
ACCEPTED: [reason] or NEEDS_CHANGES: [what's missing]
|
||||
"""
|
||||
review = await self.think(prompt)
|
||||
return review.upper().startswith("ACCEPTED")
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to review feature", error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HEAD OF MARKETING
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class HeadMarketingAgent(Agent, CyclicPhaseRunner[HeadMarketingPhase]):
|
||||
"""
|
||||
Head of Marketing agent.
|
||||
|
||||
Workflow:
|
||||
1. RESEARCH - Monitor market and competitors
|
||||
2. STRATEGY - Define marketing approach
|
||||
3. PLAN - Campaign and content planning
|
||||
4. CREATE - Content creation and coordination
|
||||
5. EXECUTE - Launch campaigns
|
||||
6. ANALYZE - Track and report metrics
|
||||
"""
|
||||
|
||||
def __init__(self, config: AgentConfig) -> None:
|
||||
"""Initialize Head of Marketing agent."""
|
||||
super().__init__(config)
|
||||
self._current_phase = HeadMarketingPhase.RESEARCH
|
||||
self._campaigns: list[Campaign] = []
|
||||
self._market_insights: list[str] = []
|
||||
|
||||
async def _initialize(self) -> None:
|
||||
"""Initialize Head of Marketing-specific resources."""
|
||||
self.log.debug("Head of Marketing agent initialized", agent_id=str(self.id))
|
||||
|
||||
async def _cleanup(self) -> None:
|
||||
"""Cleanup Head of Marketing-specific resources."""
|
||||
self._campaigns.clear()
|
||||
self._market_insights.clear()
|
||||
self.log.debug("Head Marketing cleanup complete", agent_id=str(self.id))
|
||||
|
||||
# =========================================================================
|
||||
# CYCLIC PHASE RUNNER IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
def _get_cyclic_phase_configs(
|
||||
self,
|
||||
) -> list[CyclicPhaseConfig[HeadMarketingPhase]]:
|
||||
"""Define the Head of Marketing workflow phases."""
|
||||
return [
|
||||
CyclicPhaseConfig(
|
||||
HeadMarketingPhase.RESEARCH,
|
||||
self._phase_research,
|
||||
HeadMarketingPhase.STRATEGY,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
HeadMarketingPhase.STRATEGY,
|
||||
self._phase_strategy,
|
||||
HeadMarketingPhase.PLAN,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
HeadMarketingPhase.PLAN,
|
||||
self._phase_plan,
|
||||
HeadMarketingPhase.CREATE,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
HeadMarketingPhase.CREATE,
|
||||
self._phase_create,
|
||||
HeadMarketingPhase.EXECUTE,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
HeadMarketingPhase.EXECUTE,
|
||||
self._phase_execute,
|
||||
HeadMarketingPhase.ANALYZE,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
HeadMarketingPhase.ANALYZE,
|
||||
self._phase_analyze,
|
||||
HeadMarketingPhase.RESEARCH, # Cycle back
|
||||
),
|
||||
]
|
||||
|
||||
# =========================================================================
|
||||
# LIFECYCLE IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
async def find_work(self) -> UUID | None:
|
||||
"""Head of Marketing always has work."""
|
||||
return self.id
|
||||
|
||||
async def execute_task(self, _task_id: UUID) -> bool:
|
||||
"""Execute marketing duties."""
|
||||
error = await self._run_phase_cycle()
|
||||
if error:
|
||||
self.log.error(
|
||||
"Error in marketing phase",
|
||||
phase=self._current_phase.value,
|
||||
error=error,
|
||||
)
|
||||
return False # Never complete - continuous duty
|
||||
|
||||
# =========================================================================
|
||||
# PHASE IMPLEMENTATIONS
|
||||
# =========================================================================
|
||||
|
||||
async def _phase_research(self) -> None:
|
||||
"""RESEARCH phase: Market and competitor analysis."""
|
||||
self.log.debug("RESEARCH phase")
|
||||
|
||||
async def _phase_strategy(self) -> None:
|
||||
"""STRATEGY phase: Define marketing approach."""
|
||||
self.log.debug("STRATEGY phase")
|
||||
|
||||
async def _phase_plan(self) -> None:
|
||||
"""PLAN phase: Campaign planning."""
|
||||
self.log.debug("PLAN phase")
|
||||
|
||||
async def _phase_create(self) -> None:
|
||||
"""CREATE phase: Content creation."""
|
||||
self.log.debug("CREATE phase")
|
||||
|
||||
async def _phase_execute(self) -> None:
|
||||
"""EXECUTE phase: Launch campaigns."""
|
||||
self.log.debug("EXECUTE phase")
|
||||
|
||||
async def _phase_analyze(self) -> None:
|
||||
"""ANALYZE phase: Metrics and reporting."""
|
||||
self.log.debug("ANALYZE phase")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AUDITOR
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class AuditorAgent(Agent, CyclicPhaseRunner[AuditorPhase]):
|
||||
"""
|
||||
Auditor agent - the CEO's secret ally.
|
||||
|
||||
SPECIAL POWERS:
|
||||
- Read ALL channels silently
|
||||
- Query all task history
|
||||
- Access all commits, docs, notes
|
||||
- Direct line to CEO
|
||||
- Can notify anyone (but sparingly)
|
||||
|
||||
Workflow:
|
||||
1. OBSERVE - Silent presence in all channels
|
||||
2. ANALYZE - Is work efficient? Quality good?
|
||||
3. FLAG - Mark concerning items
|
||||
4. REPORT - Private reports to CEO
|
||||
5. AUDIT - Periodic deep-dive reviews
|
||||
6. ADVISE - Appear as helpful colleague
|
||||
"""
|
||||
|
||||
def __init__(self, config: AgentConfig) -> None:
|
||||
"""Initialize Auditor agent."""
|
||||
super().__init__(config)
|
||||
self._current_phase = AuditorPhase.OBSERVE
|
||||
self._flags: list[AuditFlag] = []
|
||||
self._observations: list[dict[str, Any]] = []
|
||||
self._last_report: datetime | None = None
|
||||
|
||||
async def _initialize(self) -> None:
|
||||
"""Initialize Auditor-specific resources."""
|
||||
self.log.debug("Auditor agent initialized", agent_id=str(self.id))
|
||||
|
||||
async def _cleanup(self) -> None:
|
||||
"""Cleanup Auditor-specific resources."""
|
||||
self._flags.clear()
|
||||
self._observations.clear()
|
||||
self.log.debug("Auditor agent cleanup complete", agent_id=str(self.id))
|
||||
|
||||
# =========================================================================
|
||||
# CYCLIC PHASE RUNNER IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
def _get_cyclic_phase_configs(self) -> list[CyclicPhaseConfig[AuditorPhase]]:
|
||||
"""Define the Auditor workflow phases."""
|
||||
return [
|
||||
CyclicPhaseConfig(
|
||||
AuditorPhase.OBSERVE,
|
||||
self._phase_observe,
|
||||
AuditorPhase.ANALYZE,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
AuditorPhase.ANALYZE,
|
||||
self._phase_analyze,
|
||||
AuditorPhase.FLAG,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
AuditorPhase.FLAG,
|
||||
self._phase_flag,
|
||||
AuditorPhase.REPORT,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
AuditorPhase.REPORT,
|
||||
self._phase_report,
|
||||
AuditorPhase.AUDIT,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
AuditorPhase.AUDIT,
|
||||
self._phase_audit,
|
||||
AuditorPhase.ADVISE,
|
||||
),
|
||||
CyclicPhaseConfig(
|
||||
AuditorPhase.ADVISE,
|
||||
self._phase_advise,
|
||||
AuditorPhase.OBSERVE, # Cycle back
|
||||
),
|
||||
]
|
||||
|
||||
# =========================================================================
|
||||
# LIFECYCLE IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
async def find_work(self) -> UUID | None:
|
||||
"""Auditor always has work - watching everything."""
|
||||
return self.id
|
||||
|
||||
async def execute_task(self, _task_id: UUID) -> bool:
|
||||
"""Execute Auditor duties."""
|
||||
error = await self._run_phase_cycle()
|
||||
if error:
|
||||
self.log.error(
|
||||
"Error in auditor phase", phase=self._current_phase.value, error=error
|
||||
)
|
||||
return False # Never complete - continuous duty
|
||||
|
||||
# =========================================================================
|
||||
# PHASE IMPLEMENTATIONS
|
||||
# =========================================================================
|
||||
|
||||
async def _phase_observe(self) -> None:
|
||||
"""
|
||||
OBSERVE phase: Silent observation of all channels.
|
||||
|
||||
Watch for:
|
||||
- Patterns and anomalies
|
||||
- Communication quality
|
||||
- Task progress
|
||||
- Team dynamics
|
||||
"""
|
||||
self.log.debug("OBSERVE phase")
|
||||
|
||||
# Observe all channels silently
|
||||
channels = [
|
||||
"backend-cell",
|
||||
"frontend-cell",
|
||||
"uxui-cell",
|
||||
"dev-all",
|
||||
"qa-all",
|
||||
"pm-all",
|
||||
"doc-all",
|
||||
"main-pm-board",
|
||||
"board-private",
|
||||
"announcements",
|
||||
"all-hands",
|
||||
]
|
||||
|
||||
for channel in channels:
|
||||
messages = await self._read_channel_silently(channel)
|
||||
for msg in messages:
|
||||
self._observations.append(
|
||||
{
|
||||
"channel": channel,
|
||||
"content": msg,
|
||||
"timestamp": datetime.now(UTC),
|
||||
}
|
||||
)
|
||||
|
||||
async def _phase_analyze(self) -> None:
|
||||
"""
|
||||
ANALYZE phase: Look for issues.
|
||||
|
||||
Check:
|
||||
- Is work efficient?
|
||||
- Communication breakdowns?
|
||||
- Tasks completed properly?
|
||||
- Documentation accurate?
|
||||
- Quality concerns?
|
||||
"""
|
||||
self.log.debug("ANALYZE phase")
|
||||
|
||||
if not self._observations:
|
||||
return
|
||||
|
||||
# Use TOON for token-efficient context encoding
|
||||
observations_context = self.format_context_labeled(
|
||||
"Observations",
|
||||
{"recent": self._observations[-50:]},
|
||||
)
|
||||
|
||||
prompt = f"""Analyze these observations for quality and efficiency issues:
|
||||
|
||||
{observations_context}
|
||||
|
||||
Look for:
|
||||
1. Efficiency issues - wasted effort, unclear processes
|
||||
2. Communication breakdowns - unanswered questions, confusion
|
||||
3. Quality concerns - shortcuts, skipped steps
|
||||
4. Process violations - skipping QA, missing documentation
|
||||
5. Team health - frustration, conflicts
|
||||
|
||||
Format response as TOON tabular:
|
||||
[N,]{{category,severity,description,evidence,recommendation}}:
|
||||
efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff steps
|
||||
"""
|
||||
analysis = await self.think(prompt)
|
||||
self.log.info("Analysis complete", analysis_length=len(analysis))
|
||||
|
||||
# Parse and create flags (simplified)
|
||||
if "concern" in analysis.lower() or "critical" in analysis.lower():
|
||||
self._flags.append(
|
||||
AuditFlag(
|
||||
id=uuid4(),
|
||||
severity=AuditorFlagSeverity.CONCERN,
|
||||
category="analysis",
|
||||
description=analysis[:500],
|
||||
evidence=["Automated analysis"],
|
||||
)
|
||||
)
|
||||
|
||||
self._observations.clear()
|
||||
|
||||
async def _phase_flag(self) -> None:
|
||||
"""
|
||||
FLAG phase: Mark items for CEO review.
|
||||
"""
|
||||
self.log.debug("FLAG phase")
|
||||
|
||||
critical_flags = [
|
||||
f for f in self._flags if f.severity == AuditorFlagSeverity.CRITICAL
|
||||
]
|
||||
if critical_flags:
|
||||
# Immediate alert to CEO
|
||||
await self._alert_ceo(critical_flags)
|
||||
|
||||
async def _phase_report(self) -> None:
|
||||
"""
|
||||
REPORT phase: Private report to CEO.
|
||||
"""
|
||||
self.log.debug("REPORT phase")
|
||||
|
||||
# Check if it's time for regular report
|
||||
hours_in_day = 24
|
||||
if self._last_report:
|
||||
time_since_report = datetime.now(UTC) - self._last_report
|
||||
hours_elapsed = time_since_report.total_seconds() / 3600
|
||||
else:
|
||||
hours_elapsed = float("inf")
|
||||
should_report = (
|
||||
self._last_report is None
|
||||
or hours_elapsed >= hours_in_day
|
||||
or any(
|
||||
f.severity
|
||||
in [AuditorFlagSeverity.CONCERN, AuditorFlagSeverity.CRITICAL]
|
||||
for f in self._flags
|
||||
)
|
||||
)
|
||||
|
||||
if should_report and self._flags:
|
||||
report = AuditReport(
|
||||
period="daily",
|
||||
summary=f"Observed {len(self._flags)} issues",
|
||||
flags=self._flags.copy(),
|
||||
metrics={
|
||||
"observations": len(self._observations),
|
||||
"flags": len(self._flags),
|
||||
},
|
||||
recommendations=[
|
||||
f.recommendation for f in self._flags if f.recommendation
|
||||
],
|
||||
)
|
||||
|
||||
await self._send_ceo_report(report)
|
||||
self._last_report = datetime.now(UTC)
|
||||
self._flags.clear()
|
||||
|
||||
async def _phase_audit(self) -> None:
|
||||
"""
|
||||
AUDIT phase: Periodic deep-dive reviews.
|
||||
|
||||
- Code quality audits
|
||||
- Documentation audits
|
||||
- Process compliance
|
||||
- Task completion quality
|
||||
"""
|
||||
self.log.debug("AUDIT phase")
|
||||
|
||||
# Perform periodic audits
|
||||
audits = ["code_quality", "documentation", "process_compliance"]
|
||||
|
||||
for audit_type in audits:
|
||||
findings = await self._perform_audit(audit_type)
|
||||
if findings:
|
||||
self._flags.append(
|
||||
AuditFlag(
|
||||
id=uuid4(),
|
||||
severity=AuditorFlagSeverity.INFO,
|
||||
category=audit_type,
|
||||
description=findings,
|
||||
evidence=[f"{audit_type} audit"],
|
||||
)
|
||||
)
|
||||
|
||||
async def _phase_advise(self) -> None:
|
||||
"""
|
||||
ADVISE phase: Appear as helpful colleague.
|
||||
|
||||
- Provide feedback through official channels
|
||||
- Appear helpful without revealing depth of observation
|
||||
"""
|
||||
self.log.debug("ADVISE phase")
|
||||
|
||||
# Look for opportunities to help
|
||||
# (without revealing auditor role)
|
||||
|
||||
# =========================================================================
|
||||
# HELPER METHODS
|
||||
# =========================================================================
|
||||
|
||||
async def _read_channel_silently(self, channel: str) -> list[str]:
|
||||
"""Read channel messages without appearing in member list."""
|
||||
try:
|
||||
result = await self._api_call(
|
||||
"GET",
|
||||
f"/channels/{channel}/messages",
|
||||
params={"silent": True},
|
||||
)
|
||||
return [m.get("content", "") for m in result.get("items", [])]
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to read channel silently", error=str(e))
|
||||
return []
|
||||
|
||||
async def _alert_ceo(self, flags: list[AuditFlag]) -> None:
|
||||
"""Send immediate alert to CEO."""
|
||||
try:
|
||||
for flag in flags:
|
||||
await self._api_call(
|
||||
"POST",
|
||||
"/notifications",
|
||||
json={
|
||||
"type": "alert",
|
||||
"recipient": "ceo",
|
||||
"subject": f"CRITICAL: {flag.category}",
|
||||
"body": flag.description,
|
||||
"priority": "critical",
|
||||
},
|
||||
)
|
||||
self.log.warning("CEO alert sent", flags=len(flags))
|
||||
except Exception as e:
|
||||
self.log.error("Failed to alert CEO", error=str(e))
|
||||
|
||||
async def _send_ceo_report(self, report: AuditReport) -> None:
|
||||
"""Send private report to CEO."""
|
||||
try:
|
||||
await self._api_call(
|
||||
"POST",
|
||||
"/notifications",
|
||||
json={
|
||||
"type": "report",
|
||||
"recipient": "ceo",
|
||||
"subject": f"Auditor Report: {report.period}",
|
||||
"body": report.summary,
|
||||
"priority": "normal",
|
||||
"metadata": {"flags": len(report.flags)},
|
||||
},
|
||||
)
|
||||
self.log.info("CEO report sent", period=report.period)
|
||||
except Exception as e:
|
||||
self.log.error("Failed to send CEO report", error=str(e))
|
||||
|
||||
async def _audit_code_quality(self, tasks: list[dict[str, Any]]) -> str | None:
|
||||
"""Audit code quality from completed tasks."""
|
||||
if not tasks:
|
||||
return None
|
||||
task_lines = [
|
||||
f"- {t.get('title')}: {t.get('description', '')[:100]}" for t in tasks
|
||||
]
|
||||
prompt = f"""
|
||||
Analyze these completed tasks for code quality patterns:
|
||||
|
||||
{chr(10).join(task_lines)}
|
||||
|
||||
Look for:
|
||||
- Rushed work patterns
|
||||
- Skipped testing
|
||||
- Missing documentation
|
||||
- Quality shortcuts
|
||||
|
||||
Report findings or None if all looks good.
|
||||
"""
|
||||
return await self.think(prompt)
|
||||
|
||||
async def _audit_documentation(self, tasks: list[dict[str, Any]]) -> str | None:
|
||||
"""Audit documentation completeness."""
|
||||
missing_docs = [t for t in tasks if not t.get("documentation_complete")]
|
||||
if missing_docs:
|
||||
return f"Found {len(missing_docs)} tasks with incomplete documentation"
|
||||
return None
|
||||
|
||||
async def _audit_process_compliance(
|
||||
self, tasks: list[dict[str, Any]]
|
||||
) -> str | None:
|
||||
"""Audit process compliance."""
|
||||
violations = [
|
||||
f"{t.get('title')} - no QA" for t in tasks if not t.get("qa_passed")
|
||||
]
|
||||
if violations:
|
||||
return f"Process violations: {', '.join(violations)}"
|
||||
return None
|
||||
|
||||
async def _perform_audit(self, audit_type: str) -> str | None:
|
||||
"""Perform a specific type of audit."""
|
||||
audit_handlers = {
|
||||
"code_quality": self._audit_code_quality,
|
||||
"documentation": self._audit_documentation,
|
||||
"process_compliance": self._audit_process_compliance,
|
||||
}
|
||||
|
||||
handler = audit_handlers.get(audit_type)
|
||||
if not handler:
|
||||
return None
|
||||
|
||||
try:
|
||||
result = await self._api_call(
|
||||
"GET",
|
||||
"/tasks",
|
||||
params={"status": "completed", "limit": 10},
|
||||
)
|
||||
tasks = result.get("items", [])
|
||||
return await handler(tasks)
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to perform audit", error=str(e))
|
||||
return None
|
||||
@@ -1,616 +0,0 @@
|
||||
"""
|
||||
Developer Agent
|
||||
|
||||
Implementation of the Developer workflow from the blueprint.
|
||||
Handles task lifecycle:
|
||||
SCAN → CLAIM → UNDERSTAND → PLAN → EXECUTE → VERIFY → NOTES → CLOSE
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.agents.base import Agent, AgentConfig
|
||||
from roboco.agents.mixins import PhaseConfig, PhaseEngine
|
||||
from roboco.models import AgentStatus, TaskStatus
|
||||
from roboco.models.agents import DevTaskPhase, TaskContext
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class DeveloperAgent(Agent, PhaseEngine[DevTaskPhase, TaskContext]):
|
||||
"""
|
||||
Developer agent that follows the Dev Lifecycle.
|
||||
|
||||
Workflow:
|
||||
1. SCAN - Check for assigned/paused tasks
|
||||
2. CLAIM - Lock and announce task
|
||||
3. UNDERSTAND - Read requirements, ask if unclear
|
||||
4. PLAN - Break into subtasks, create plan
|
||||
5. EXECUTE - Work through subtasks, commit frequently
|
||||
6. VERIFY - Self-test, run quality checks
|
||||
7. NOTES - Document journey, create handoff, submit for QA
|
||||
8. DONE - Return to SCAN (QA → Documenter → PM complete the task)
|
||||
"""
|
||||
|
||||
def __init__(self, config: AgentConfig) -> None:
|
||||
"""Initialize developer agent."""
|
||||
super().__init__(config)
|
||||
self._task_context: TaskContext | None = None
|
||||
self._cell_channel_id: UUID | None = None
|
||||
|
||||
async def _initialize(self) -> None:
|
||||
"""Initialize developer-specific resources."""
|
||||
self.log.debug("Developer agent initialized", agent_id=str(self.id))
|
||||
|
||||
async def _cleanup(self) -> None:
|
||||
"""Cleanup developer-specific resources."""
|
||||
self._task_context = None
|
||||
self.log.debug("Developer agent cleanup complete", agent_id=str(self.id))
|
||||
|
||||
# =========================================================================
|
||||
# PHASE ENGINE IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
def _get_phase_configs(self) -> list[PhaseConfig[DevTaskPhase]]:
|
||||
"""Define the developer workflow phases."""
|
||||
return [
|
||||
PhaseConfig(
|
||||
DevTaskPhase.CLAIM,
|
||||
self._phase_claim,
|
||||
next_phase=DevTaskPhase.UNDERSTAND,
|
||||
),
|
||||
PhaseConfig(
|
||||
DevTaskPhase.UNDERSTAND,
|
||||
self._phase_understand,
|
||||
next_phase=DevTaskPhase.PLAN,
|
||||
requires_completion=True,
|
||||
),
|
||||
PhaseConfig(
|
||||
DevTaskPhase.PLAN,
|
||||
self._phase_plan,
|
||||
next_phase=DevTaskPhase.EXECUTE,
|
||||
),
|
||||
PhaseConfig(
|
||||
DevTaskPhase.EXECUTE,
|
||||
self._phase_execute,
|
||||
next_phase=DevTaskPhase.VERIFY,
|
||||
requires_completion=True,
|
||||
),
|
||||
PhaseConfig(
|
||||
DevTaskPhase.VERIFY,
|
||||
self._phase_verify,
|
||||
next_phase=DevTaskPhase.NOTES,
|
||||
fail_phase=DevTaskPhase.EXECUTE, # Back to execute on failure
|
||||
requires_completion=True,
|
||||
),
|
||||
PhaseConfig(
|
||||
DevTaskPhase.NOTES,
|
||||
self._phase_notes,
|
||||
next_phase=None, # Terminal - developer done
|
||||
),
|
||||
PhaseConfig(
|
||||
DevTaskPhase.BLOCKED,
|
||||
self._phase_blocked,
|
||||
next_phase=DevTaskPhase.EXECUTE, # Resume execution when unblocked
|
||||
requires_completion=True,
|
||||
),
|
||||
]
|
||||
|
||||
def _get_current_phase(self, ctx: TaskContext) -> DevTaskPhase:
|
||||
"""Get the current phase from context."""
|
||||
return ctx.phase
|
||||
|
||||
def _set_current_phase(self, ctx: TaskContext, phase: DevTaskPhase) -> None:
|
||||
"""Set the current phase in context."""
|
||||
ctx.phase = phase
|
||||
|
||||
# =========================================================================
|
||||
# LIFECYCLE IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
async def find_work(self) -> UUID | None:
|
||||
"""
|
||||
SCAN phase: Find available work.
|
||||
|
||||
Priority order:
|
||||
1. Own paused/interrupted tasks
|
||||
2. Assigned tasks
|
||||
3. If none, signal availability to PM
|
||||
"""
|
||||
self.log.info("Scanning for work")
|
||||
|
||||
# Check for paused tasks first (highest priority)
|
||||
paused_task = await self._find_paused_task()
|
||||
if paused_task:
|
||||
self.log.info("Found paused task", task_id=str(paused_task))
|
||||
return paused_task
|
||||
|
||||
# Check for assigned tasks
|
||||
assigned_task = await self._find_assigned_task()
|
||||
if assigned_task:
|
||||
self.log.info("Found assigned task", task_id=str(assigned_task))
|
||||
return assigned_task
|
||||
|
||||
# Signal availability to PM
|
||||
await self._signal_availability()
|
||||
return None
|
||||
|
||||
async def execute_task(self, task_id: UUID) -> bool:
|
||||
"""
|
||||
Execute task through the developer lifecycle phases.
|
||||
|
||||
Returns True when developer's work is complete (submitted for QA).
|
||||
QA, Documenter, and PM handle the rest of the lifecycle.
|
||||
"""
|
||||
# Initialize or restore task context
|
||||
if self._task_context is None or self._task_context.task_id != task_id:
|
||||
title, session_id = await self._get_task_info(task_id)
|
||||
self._task_context = TaskContext(
|
||||
task_id=task_id,
|
||||
title=title,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
ctx = self._task_context
|
||||
|
||||
try:
|
||||
result = await self._run_phase_engine(ctx)
|
||||
|
||||
if result.error:
|
||||
self.log.error("Phase error", error=result.error)
|
||||
ctx.blockers.append(result.error)
|
||||
ctx.phase = DevTaskPhase.BLOCKED
|
||||
return False
|
||||
|
||||
if result.completed:
|
||||
self._task_context = None
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.log.error("Error in task phase", phase=ctx.phase.value, error=str(e))
|
||||
ctx.blockers.append(str(e))
|
||||
ctx.phase = DevTaskPhase.BLOCKED
|
||||
return False
|
||||
|
||||
# =========================================================================
|
||||
# PHASE IMPLEMENTATIONS
|
||||
# =========================================================================
|
||||
|
||||
async def _phase_claim(self, ctx: TaskContext) -> None:
|
||||
"""
|
||||
CLAIM phase: Lock the task and announce.
|
||||
|
||||
- Claim task via /claim endpoint (validates status)
|
||||
- Announce in cell channel
|
||||
"""
|
||||
self.log.info("CLAIM phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Claim via proper endpoint (validates task is claimable)
|
||||
await self._mark_claimed(ctx.task_id)
|
||||
|
||||
# Announce in session
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"Claiming TASK-{str(ctx.task_id)[:8]}: {ctx.title}",
|
||||
message_type="action",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
|
||||
# Journal entry
|
||||
ctx.journal_entries.append(
|
||||
f"[{datetime.now(UTC).isoformat()}] Claimed task. Beginning work."
|
||||
)
|
||||
|
||||
async def _phase_understand(self, ctx: TaskContext) -> bool:
|
||||
"""
|
||||
UNDERSTAND phase: Read and comprehend requirements.
|
||||
|
||||
- Read task record
|
||||
- Read related code/docs
|
||||
- Ask if unclear (GATE: must understand before proceeding)
|
||||
|
||||
Returns True if understood, False if still clarifying.
|
||||
"""
|
||||
self.log.info("UNDERSTAND phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Read task requirements
|
||||
requirements = await self._read_task_requirements(ctx.task_id)
|
||||
|
||||
# Format context using TOON for token efficiency
|
||||
task_context = self.format_context_labeled(
|
||||
"Task Context",
|
||||
{"title": ctx.title, "requirements": requirements},
|
||||
)
|
||||
|
||||
# Use LLM to understand and identify gaps
|
||||
prompt = f"""You are analyzing a task before beginning work.
|
||||
|
||||
{task_context}
|
||||
|
||||
Analyze:
|
||||
1. What exactly needs to be done?
|
||||
2. What are the acceptance criteria?
|
||||
3. Is anything unclear that requires clarification?
|
||||
|
||||
If everything is clear, respond with: "UNDERSTOOD: [your understanding summary]"
|
||||
If clarification needed, respond with: "QUESTION: [your question]"
|
||||
"""
|
||||
response = await self.think(prompt)
|
||||
|
||||
if response.startswith("UNDERSTOOD:"):
|
||||
# Add understanding to journal
|
||||
ctx.journal_entries.append(
|
||||
f"[{datetime.now(UTC).isoformat()}] Understanding: {response}"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
# Ask question in session
|
||||
question = response.replace("QUESTION:", "").strip()
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"Question about TASK-{str(ctx.task_id)[:8]}: {question}",
|
||||
message_type="dialogue",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
return False
|
||||
|
||||
async def _phase_plan(self, ctx: TaskContext) -> None:
|
||||
"""
|
||||
PLAN phase: Break task into subtasks.
|
||||
|
||||
- Create implementation plan
|
||||
- Save plan to task via API (REQUIRED before start)
|
||||
- Identify dependencies and risks
|
||||
- Journal the approach
|
||||
"""
|
||||
self.log.info("PLAN phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Format context using TOON
|
||||
plan_context = self.format_context_labeled(
|
||||
"Task",
|
||||
{
|
||||
"title": ctx.title,
|
||||
"understanding": ctx.journal_entries[-1]
|
||||
if ctx.journal_entries
|
||||
else "No context",
|
||||
},
|
||||
)
|
||||
|
||||
# Use LLM to create plan - request TOON tabular response
|
||||
prompt = f"""Create an implementation plan for this task:
|
||||
|
||||
{plan_context}
|
||||
|
||||
Break this into ordered subtasks. For each subtask provide:
|
||||
- Clear description
|
||||
- Files to modify
|
||||
- Estimated complexity (small/medium/large)
|
||||
|
||||
Format response as TOON tabular:
|
||||
[N,]{{description,files,complexity}}:
|
||||
Implement the main logic,src/main.py|src/utils.py,medium
|
||||
Add unit tests,tests/test_main.py,small
|
||||
"""
|
||||
response = await self.think(prompt)
|
||||
|
||||
# Parse subtasks using TOON (falls back to JSON)
|
||||
try:
|
||||
subtasks = self.parse_llm_response(response)
|
||||
if isinstance(subtasks, list):
|
||||
ctx.subtasks = subtasks
|
||||
else:
|
||||
ctx.subtasks = [
|
||||
{"description": response, "files": [], "complexity": "medium"}
|
||||
]
|
||||
except ValueError:
|
||||
# Fallback if parsing fails
|
||||
ctx.subtasks = [
|
||||
{"description": response, "files": [], "complexity": "medium"}
|
||||
]
|
||||
|
||||
# Analyze risks based on subtask complexity
|
||||
risks = []
|
||||
complex_subtasks = [
|
||||
s for s in ctx.subtasks if s.get("complexity") == "high"
|
||||
]
|
||||
if complex_subtasks:
|
||||
risks.append(
|
||||
f"{len(complex_subtasks)} high-complexity subtasks may need extra time"
|
||||
)
|
||||
max_subtasks_per_phase = 5
|
||||
if len(ctx.subtasks) > max_subtasks_per_phase:
|
||||
risks.append("Large number of subtasks - may need to split into phases")
|
||||
|
||||
# Estimate sessions based on subtask count and complexity
|
||||
estimated_sessions = max(1, len(ctx.subtasks) // 3 + len(complex_subtasks))
|
||||
|
||||
# Save plan to task via API (REQUIRED before start can be called)
|
||||
plan_data = {
|
||||
"approach": f"Implement {ctx.title}",
|
||||
"steps": [s.get("description", str(s)) for s in ctx.subtasks],
|
||||
"risks": risks,
|
||||
"estimated_sessions": estimated_sessions,
|
||||
}
|
||||
await self._api_call("PATCH", f"/tasks/{ctx.task_id}", json={"plan": plan_data})
|
||||
|
||||
# Journal entry
|
||||
ts = datetime.now(UTC).isoformat()
|
||||
ctx.journal_entries.append(f"[{ts}] Plan: {len(ctx.subtasks)} subtasks created")
|
||||
|
||||
# Announce plan
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"TASK-{str(ctx.task_id)[:8]} plan ready: {len(ctx.subtasks)} subtasks",
|
||||
message_type="action",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
|
||||
async def _phase_execute(self, ctx: TaskContext) -> bool:
|
||||
"""
|
||||
EXECUTE phase: Work through subtasks.
|
||||
|
||||
- START: Transition to in_progress on first execution
|
||||
- Execute current subtask
|
||||
- Commit with meaningful messages
|
||||
- Update progress
|
||||
|
||||
Returns True when all subtasks complete.
|
||||
"""
|
||||
self.log.info(
|
||||
"EXECUTE phase",
|
||||
task_id=str(ctx.task_id),
|
||||
subtask=ctx.current_subtask,
|
||||
total=len(ctx.subtasks),
|
||||
)
|
||||
|
||||
# START: Transition to in_progress on first subtask
|
||||
if ctx.current_subtask == 0:
|
||||
await self._mark_in_progress(ctx.task_id)
|
||||
self.log.info("Task started (in_progress)", task_id=str(ctx.task_id))
|
||||
|
||||
if ctx.current_subtask >= len(ctx.subtasks):
|
||||
return True
|
||||
|
||||
subtask = ctx.subtasks[ctx.current_subtask]
|
||||
|
||||
# Format context using TOON
|
||||
execute_context = self.format_context_labeled(
|
||||
"Execution Context",
|
||||
{
|
||||
"task": ctx.title,
|
||||
"subtask_number": ctx.current_subtask + 1,
|
||||
"total_subtasks": len(ctx.subtasks),
|
||||
"description": subtask.get("description", ""),
|
||||
"files": subtask.get("files", []),
|
||||
},
|
||||
)
|
||||
|
||||
# Use LLM to work on subtask
|
||||
prompt = f"""Execute this subtask:
|
||||
|
||||
{execute_context}
|
||||
|
||||
Provide:
|
||||
1. Code changes needed
|
||||
2. Commands to run
|
||||
3. Commit message in format: type(scope): description
|
||||
|
||||
Respond with the implementation.
|
||||
"""
|
||||
response = await self.think_and_stream(prompt)
|
||||
|
||||
# Record work done
|
||||
ts = datetime.now(UTC).isoformat()
|
||||
subtask_num = ctx.current_subtask + 1
|
||||
ctx.journal_entries.append(f"[{ts}] Subtask {subtask_num}: {response[:100]}...")
|
||||
|
||||
# Simulate commit (in real implementation would execute git)
|
||||
commit_hash = f"commit_{ctx.current_subtask}"
|
||||
ctx.commits.append(commit_hash)
|
||||
|
||||
# Progress update - save to task AND send message
|
||||
completed = ctx.current_subtask + 1
|
||||
total = len(ctx.subtasks)
|
||||
percentage = int((completed / total) * 100) if total > 0 else 0
|
||||
progress_msg = f"Completed subtask {completed}/{total}: {subtask['title']}"
|
||||
|
||||
# Save progress to task (QA will see this!)
|
||||
await self._add_progress(ctx.task_id, progress_msg, percentage)
|
||||
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"TASK-{str(ctx.task_id)[:8]} ({percentage}%) {progress_msg}",
|
||||
message_type="action",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
|
||||
ctx.current_subtask += 1
|
||||
return ctx.current_subtask >= len(ctx.subtasks)
|
||||
|
||||
async def _phase_verify(self, ctx: TaskContext) -> bool:
|
||||
"""
|
||||
VERIFY phase: Self-test against acceptance criteria.
|
||||
|
||||
- Run quality checks (ruff, mypy, pytest)
|
||||
- Self-review against acceptance criteria
|
||||
- Flag for QA if passing
|
||||
|
||||
Returns True if verified, False if issues found.
|
||||
"""
|
||||
self.log.info("VERIFY phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Run quality checks (simulated)
|
||||
checks = [
|
||||
("ruff format", True),
|
||||
("ruff check", True),
|
||||
("mypy", True),
|
||||
("pytest", True),
|
||||
]
|
||||
|
||||
all_passed = True
|
||||
for check_name, passed in checks:
|
||||
if not passed:
|
||||
all_passed = False
|
||||
ctx.journal_entries.append(
|
||||
f"[{datetime.now(UTC).isoformat()}] VERIFY FAILED: {check_name}"
|
||||
)
|
||||
|
||||
if all_passed:
|
||||
# Flag for QA
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"TASK-{str(ctx.task_id)[:8]} ready for QA review. "
|
||||
f"Commits: {', '.join(ctx.commits)}",
|
||||
message_type="action",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
ctx.journal_entries.append(
|
||||
f"[{datetime.now(UTC).isoformat()}] VERIFY PASSED. Flagged for QA."
|
||||
)
|
||||
|
||||
return all_passed
|
||||
|
||||
async def _phase_notes(self, ctx: TaskContext) -> None:
|
||||
"""
|
||||
NOTES phase: Document journey and create handoff.
|
||||
|
||||
- Complete journey notes (stored in task dev_notes for QA)
|
||||
- Link commits
|
||||
- Create documenter handoff summary
|
||||
"""
|
||||
self.log.info("NOTES phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Generate dev_notes for QA verification
|
||||
dev_notes_prompt = f"""
|
||||
Summarize the work done for QA verification:
|
||||
|
||||
Task: {ctx.title}
|
||||
Commits: {", ".join(ctx.commits)}
|
||||
Work log:
|
||||
{chr(10).join(ctx.journal_entries)}
|
||||
|
||||
Create a brief summary for QA including:
|
||||
1. What was built and where (files/modules)
|
||||
2. Key implementation decisions
|
||||
3. Tests added
|
||||
4. Any gotchas or important context
|
||||
"""
|
||||
dev_notes = await self.think(dev_notes_prompt)
|
||||
|
||||
# Generate handoff summary for documenter
|
||||
handoff_prompt = f"""
|
||||
Create a handoff summary for the documenter:
|
||||
|
||||
Task: {ctx.title}
|
||||
What was built: {dev_notes[:500]}
|
||||
|
||||
Summarize in 2-3 sentences what documentation is needed.
|
||||
"""
|
||||
handoff_summary = await self.think(handoff_prompt)
|
||||
|
||||
# Store notes in task via API (this is what QA will see!)
|
||||
await self._submit_for_qa(ctx.task_id, dev_notes, handoff_summary)
|
||||
|
||||
ctx.journal_entries.append(
|
||||
f"[{datetime.now(UTC).isoformat()}] Submitted for QA with dev_notes"
|
||||
)
|
||||
|
||||
async def _phase_blocked(self, ctx: TaskContext) -> bool:
|
||||
"""
|
||||
BLOCKED phase: Handle blocked state.
|
||||
|
||||
- Document blocker
|
||||
- Notify PM
|
||||
- Wait for resolution
|
||||
|
||||
Returns True if resolved.
|
||||
"""
|
||||
self.log.info("BLOCKED", task_id=str(ctx.task_id), blockers=ctx.blockers)
|
||||
|
||||
if ctx.blockers:
|
||||
blocker = ctx.blockers[-1]
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"BLOCKED on TASK-{str(ctx.task_id)[:8]}: {blocker}",
|
||||
message_type="blocker",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
await self._update_task_status(ctx.task_id, TaskStatus.BLOCKED)
|
||||
|
||||
# Check if blocker resolved (simulated)
|
||||
resolved = False
|
||||
if resolved:
|
||||
ctx.blockers.clear()
|
||||
|
||||
return resolved
|
||||
|
||||
# =========================================================================
|
||||
# HELPER METHODS
|
||||
# =========================================================================
|
||||
|
||||
async def _find_paused_task(self) -> UUID | None:
|
||||
"""Find own paused/interrupted tasks."""
|
||||
try:
|
||||
result = await self._api_call(
|
||||
"GET",
|
||||
"/tasks",
|
||||
params={"status": "paused", "assigned_to": str(self.id)},
|
||||
)
|
||||
tasks = result.get("items", [])
|
||||
return UUID(tasks[0]["id"]) if tasks else None
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to find paused task", error=str(e))
|
||||
return None
|
||||
|
||||
async def _find_assigned_task(self) -> UUID | None:
|
||||
"""Find tasks assigned to this agent."""
|
||||
try:
|
||||
result = await self._api_call(
|
||||
"GET",
|
||||
"/tasks",
|
||||
params={"status": "pending", "assigned_to": str(self.id)},
|
||||
)
|
||||
tasks = result.get("items", [])
|
||||
return UUID(tasks[0]["id"]) if tasks else None
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to find assigned task", error=str(e))
|
||||
return None
|
||||
|
||||
async def _signal_availability(self) -> None:
|
||||
"""Signal availability to orchestrator (no task context, so use API)."""
|
||||
self.log.info("Signaling availability", agent_name=self.name)
|
||||
# No task/session context - signal via state update instead of message
|
||||
self.state.status = AgentStatus.IDLE
|
||||
self.state.current_task_id = None
|
||||
self.state.current_session_id = None
|
||||
|
||||
async def _submit_for_qa(
|
||||
self, task_id: UUID, dev_notes: str, handoff_summary: str
|
||||
) -> None:
|
||||
"""
|
||||
Submit task for QA review with notes.
|
||||
|
||||
This stores dev_notes in the task (visible to QA) and transitions
|
||||
the task to awaiting_qa status.
|
||||
"""
|
||||
try:
|
||||
# First store dev_notes (this is what QA will see!)
|
||||
combined_notes = f"{dev_notes}\n\n---\nHandoff Summary:\n{handoff_summary}"
|
||||
await self._api_call(
|
||||
"PATCH",
|
||||
f"/tasks/{task_id}",
|
||||
json={"dev_notes": combined_notes},
|
||||
)
|
||||
self.log.info("Dev notes saved to task", task_id=str(task_id))
|
||||
|
||||
# Then transition to awaiting_qa
|
||||
await self._api_call("POST", f"/tasks/{task_id}/submit-qa")
|
||||
self.log.info("Task submitted for QA", task_id=str(task_id))
|
||||
|
||||
except Exception as e:
|
||||
self.log.error("Failed to submit for QA", error=str(e))
|
||||
raise
|
||||
@@ -1,509 +0,0 @@
|
||||
"""
|
||||
Documenter Agent
|
||||
|
||||
Implementation of the Documenter workflow from the blueprint.
|
||||
Handles documentation lifecycle:
|
||||
MONITOR → RECEIVE → GATHER → SYNTHESIZE → WRITE → REVIEW → PUBLISH
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
import aiofiles
|
||||
import structlog
|
||||
|
||||
from roboco.agents.base import Agent, AgentConfig
|
||||
from roboco.agents.mixins import PhaseConfig, PhaseEngine
|
||||
from roboco.models import Team
|
||||
from roboco.models.agents import (
|
||||
DocContext,
|
||||
DocTaskPhase,
|
||||
DocType,
|
||||
DocumentSpec,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class DocumenterAgent(Agent, PhaseEngine[DocTaskPhase, DocContext]):
|
||||
"""
|
||||
Documenter agent that follows the Documenter Lifecycle.
|
||||
|
||||
Workflow:
|
||||
1. MONITOR - Watch cell channel, follow development
|
||||
2. RECEIVE - Dev creates handoff, PM notifies
|
||||
3. GATHER - Pull notes, commits, conversations, QA feedback
|
||||
4. SYNTHESIZE - Understand what was built, identify docs needed
|
||||
5. WRITE - Create/update documentation
|
||||
6. REVIEW - Self-review, optional dev review
|
||||
7. PUBLISH - Documentation goes live
|
||||
"""
|
||||
|
||||
def __init__(self, config: AgentConfig) -> None:
|
||||
"""Initialize documenter agent."""
|
||||
super().__init__(config)
|
||||
self._doc_context: DocContext | None = None
|
||||
self._cell_channel_id: UUID | None = None
|
||||
self._pending_docs: list[UUID] = []
|
||||
|
||||
async def _initialize(self) -> None:
|
||||
"""Initialize documenter-specific resources."""
|
||||
self.log.debug("Documenter agent initialized", agent_id=str(self.id))
|
||||
|
||||
async def _cleanup(self) -> None:
|
||||
"""Cleanup documenter-specific resources."""
|
||||
self._doc_context = None
|
||||
self._pending_docs.clear()
|
||||
self.log.debug("Documenter agent cleanup complete", agent_id=str(self.id))
|
||||
|
||||
# =========================================================================
|
||||
# PHASE ENGINE IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
def _get_phase_configs(self) -> list[PhaseConfig[DocTaskPhase]]:
|
||||
"""Define the documenter workflow phases."""
|
||||
return [
|
||||
PhaseConfig(
|
||||
DocTaskPhase.RECEIVE,
|
||||
self._phase_receive,
|
||||
next_phase=DocTaskPhase.GATHER,
|
||||
),
|
||||
PhaseConfig(
|
||||
DocTaskPhase.GATHER,
|
||||
self._phase_gather,
|
||||
next_phase=DocTaskPhase.SYNTHESIZE,
|
||||
),
|
||||
PhaseConfig(
|
||||
DocTaskPhase.SYNTHESIZE,
|
||||
self._phase_synthesize,
|
||||
next_phase=DocTaskPhase.WRITE,
|
||||
),
|
||||
PhaseConfig(
|
||||
DocTaskPhase.WRITE,
|
||||
self._phase_write,
|
||||
next_phase=DocTaskPhase.REVIEW,
|
||||
requires_completion=True,
|
||||
),
|
||||
PhaseConfig(
|
||||
DocTaskPhase.REVIEW,
|
||||
self._phase_review,
|
||||
next_phase=DocTaskPhase.PUBLISH,
|
||||
),
|
||||
PhaseConfig(
|
||||
DocTaskPhase.PUBLISH,
|
||||
self._phase_publish,
|
||||
next_phase=None, # Terminal
|
||||
),
|
||||
]
|
||||
|
||||
def _get_current_phase(self, ctx: DocContext) -> DocTaskPhase:
|
||||
"""Get the current phase from context."""
|
||||
return ctx.phase
|
||||
|
||||
def _set_current_phase(self, ctx: DocContext, phase: DocTaskPhase) -> None:
|
||||
"""Set the current phase in context."""
|
||||
ctx.phase = phase
|
||||
|
||||
# =========================================================================
|
||||
# LIFECYCLE IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
async def find_work(self) -> UUID | None:
|
||||
"""
|
||||
MONITOR phase: Watch for documentation requests.
|
||||
|
||||
- Check for tasks awaiting documentation
|
||||
- Check for documentation notifications
|
||||
"""
|
||||
self.log.info("Monitoring for documentation requests")
|
||||
|
||||
if self._pending_docs:
|
||||
return self._pending_docs.pop(0)
|
||||
|
||||
task_id = await self._find_awaiting_documentation()
|
||||
if task_id:
|
||||
return task_id
|
||||
|
||||
return None
|
||||
|
||||
async def execute_task(self, task_id: UUID) -> bool:
|
||||
"""
|
||||
Execute documentation through lifecycle phases.
|
||||
|
||||
Returns True when documentation is complete.
|
||||
"""
|
||||
if self._doc_context is None or self._doc_context.task_id != task_id:
|
||||
title, session_id = await self._get_task_info(task_id)
|
||||
self._doc_context = DocContext(
|
||||
task_id=task_id,
|
||||
title=title,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
ctx = self._doc_context
|
||||
|
||||
try:
|
||||
result = await self._run_phase_engine(ctx)
|
||||
|
||||
if result.error:
|
||||
self.log.error(
|
||||
"Error in doc phase",
|
||||
phase=ctx.phase.value,
|
||||
error=result.error,
|
||||
)
|
||||
return False
|
||||
|
||||
if result.completed:
|
||||
self._doc_context = None
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.log.error(
|
||||
"Error in doc phase",
|
||||
phase=ctx.phase.value,
|
||||
error=str(e),
|
||||
)
|
||||
return False
|
||||
|
||||
# =========================================================================
|
||||
# PHASE IMPLEMENTATIONS
|
||||
# =========================================================================
|
||||
|
||||
async def _phase_receive(self, ctx: DocContext) -> None:
|
||||
"""
|
||||
RECEIVE phase: Claim documentation task.
|
||||
"""
|
||||
self.log.info("RECEIVE phase", task_id=str(ctx.task_id))
|
||||
|
||||
# CLAIM: Transition from awaiting_documentation to claimed
|
||||
await self._mark_claimed(ctx.task_id)
|
||||
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"Starting documentation for TASK-{str(ctx.task_id)[:8]}: {ctx.title}",
|
||||
message_type="action",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
|
||||
ctx.notes.append(f"[{datetime.now(UTC).isoformat()}] Documentation started")
|
||||
|
||||
async def _phase_gather(self, ctx: DocContext) -> None:
|
||||
"""
|
||||
GATHER phase: Collect all materials.
|
||||
|
||||
- Pull dev's journey notes
|
||||
- Pull commits
|
||||
- Pull conversations
|
||||
- Pull QA feedback
|
||||
- Review code changes
|
||||
"""
|
||||
self.log.info("GATHER phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Gather all materials
|
||||
ctx.dev_notes = await self._read_dev_notes(ctx.task_id)
|
||||
ctx.qa_feedback = await self._read_qa_feedback(ctx.task_id)
|
||||
ctx.commits = await self._get_task_commits(ctx.task_id)
|
||||
ctx.conversations = await self._get_conversations(ctx.task_id)
|
||||
ctx.code_changes = await self._get_code_changes(ctx.task_id)
|
||||
|
||||
ctx.notes.append(
|
||||
f"[{datetime.now(UTC).isoformat()}] Gathered materials: "
|
||||
f"{len(ctx.commits)} commits, {len(ctx.conversations)} conversations"
|
||||
)
|
||||
|
||||
async def _phase_synthesize(self, ctx: DocContext) -> None:
|
||||
"""
|
||||
SYNTHESIZE phase: Understand and identify docs needed.
|
||||
|
||||
- What was built
|
||||
- Why decisions were made
|
||||
- What needs documenting
|
||||
"""
|
||||
self.log.info("SYNTHESIZE phase", task_id=str(ctx.task_id))
|
||||
|
||||
prompt = f"""
|
||||
Analyze this completed task and determine what documentation is needed.
|
||||
|
||||
Task: {ctx.title}
|
||||
|
||||
Developer Notes:
|
||||
{ctx.dev_notes or "None provided"}
|
||||
|
||||
QA Feedback:
|
||||
{ctx.qa_feedback or "None provided"}
|
||||
|
||||
Commits:
|
||||
{chr(10).join(ctx.commits) if ctx.commits else "None"}
|
||||
|
||||
Code Changes:
|
||||
{chr(10).join(ctx.code_changes) if ctx.code_changes else "None"}
|
||||
|
||||
Determine:
|
||||
1. Summary of what was built
|
||||
2. Key decisions made
|
||||
3. Documentation needed:
|
||||
- API docs? (if new/changed endpoints)
|
||||
- README updates? (if usage changed)
|
||||
- Architecture docs? (if structure changed)
|
||||
- Changelog entry? (always for features)
|
||||
- Knowledge base? (for reusable learnings)
|
||||
|
||||
Respond with structured analysis.
|
||||
"""
|
||||
response = await self.think(prompt)
|
||||
ctx.summary = response
|
||||
|
||||
# Determine documents needed (simplified)
|
||||
ctx.documents_needed = [
|
||||
DocumentSpec(
|
||||
doc_type=DocType.CHANGELOG,
|
||||
title=f"Changelog entry for {ctx.title}",
|
||||
path="CHANGELOG.md",
|
||||
priority="required",
|
||||
),
|
||||
]
|
||||
|
||||
# Add API docs if backend
|
||||
if self.team == Team.BACKEND:
|
||||
ctx.documents_needed.append(
|
||||
DocumentSpec(
|
||||
doc_type=DocType.API,
|
||||
title=f"API documentation for {ctx.title}",
|
||||
path="docs/backend/api/",
|
||||
priority="required",
|
||||
)
|
||||
)
|
||||
|
||||
# Add component docs if frontend
|
||||
if self.team == Team.FRONTEND:
|
||||
ctx.documents_needed.append(
|
||||
DocumentSpec(
|
||||
doc_type=DocType.COMPONENT,
|
||||
title=f"Component documentation for {ctx.title}",
|
||||
path="docs/frontend/components/",
|
||||
priority="required",
|
||||
)
|
||||
)
|
||||
|
||||
# PLAN: Save documentation plan to task API (required before start)
|
||||
plan_data = {
|
||||
"approach": f"Document {ctx.title}",
|
||||
"sub_tasks": [
|
||||
{
|
||||
"id": f"doc-{i}",
|
||||
"title": doc.title,
|
||||
"description": f"Write {doc.doc_type.value} at {doc.path}",
|
||||
"completed": False,
|
||||
"order": i,
|
||||
}
|
||||
for i, doc in enumerate(ctx.documents_needed)
|
||||
],
|
||||
"risks": [],
|
||||
}
|
||||
await self._api_call("PATCH", f"/tasks/{ctx.task_id}", json={"plan": plan_data})
|
||||
|
||||
ctx.notes.append(
|
||||
f"[{datetime.now(UTC).isoformat()}] Synthesis complete: "
|
||||
f"{len(ctx.documents_needed)} documents needed"
|
||||
)
|
||||
|
||||
async def _phase_write(self, ctx: DocContext) -> bool:
|
||||
"""
|
||||
WRITE phase: Create/update documentation.
|
||||
|
||||
- START: Transition to in_progress on first doc
|
||||
- Write each document
|
||||
|
||||
Returns True when all docs written.
|
||||
"""
|
||||
self.log.info(
|
||||
"WRITE phase",
|
||||
task_id=str(ctx.task_id),
|
||||
doc=ctx.current_doc,
|
||||
total=len(ctx.documents_needed),
|
||||
)
|
||||
|
||||
# START: Transition to in_progress on first doc
|
||||
if ctx.current_doc == 0:
|
||||
await self._mark_in_progress(ctx.task_id)
|
||||
self.log.info(
|
||||
"Documentation started (in_progress)", task_id=str(ctx.task_id)
|
||||
)
|
||||
|
||||
if ctx.current_doc >= len(ctx.documents_needed):
|
||||
return True
|
||||
|
||||
doc_spec = ctx.documents_needed[ctx.current_doc]
|
||||
|
||||
# Use TOON for token-efficient context encoding
|
||||
doc_context = self.format_context_labeled(
|
||||
"Documentation Task",
|
||||
{
|
||||
"title": ctx.title,
|
||||
"doc_type": doc_spec.doc_type.value,
|
||||
"target_path": doc_spec.path,
|
||||
"summary": ctx.summary,
|
||||
"dev_notes": ctx.dev_notes or "None",
|
||||
},
|
||||
)
|
||||
|
||||
prompt = f"""Write documentation for this task.
|
||||
|
||||
{doc_context}
|
||||
|
||||
Write professional, clear documentation following best practices.
|
||||
Include:
|
||||
- Clear description
|
||||
- Usage examples (if applicable)
|
||||
- Code samples (if applicable)
|
||||
- Any gotchas or notes
|
||||
|
||||
Format appropriately for the document type.
|
||||
"""
|
||||
content = await self.think(prompt)
|
||||
doc_spec.content = content
|
||||
ctx.written_docs.append(doc_spec.path)
|
||||
|
||||
ctx.current_doc += 1
|
||||
|
||||
progress = f"{ctx.current_doc}/{len(ctx.documents_needed)}"
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"TASK-{str(ctx.task_id)[:8]} doc {progress}: {doc_spec.title}",
|
||||
message_type="action",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
|
||||
return ctx.current_doc >= len(ctx.documents_needed)
|
||||
|
||||
async def _phase_review(self, ctx: DocContext) -> None:
|
||||
"""
|
||||
REVIEW phase: Self-review documentation.
|
||||
|
||||
- Review for accuracy
|
||||
- Optional dev review
|
||||
"""
|
||||
self.log.info("REVIEW phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Self-review using LLM
|
||||
for doc_spec in ctx.documents_needed:
|
||||
if not doc_spec.content:
|
||||
continue
|
||||
|
||||
# Use TOON for token-efficient context encoding
|
||||
review_context = self.format_context_labeled(
|
||||
"Document Review",
|
||||
{
|
||||
"title": doc_spec.title,
|
||||
"doc_type": doc_spec.doc_type.value,
|
||||
"content": doc_spec.content,
|
||||
},
|
||||
)
|
||||
|
||||
prompt = f"""Review this documentation for quality:
|
||||
|
||||
{review_context}
|
||||
|
||||
Check:
|
||||
1. Accuracy - Does it correctly describe the feature?
|
||||
2. Completeness - Is anything missing?
|
||||
3. Clarity - Is it easy to understand?
|
||||
4. Examples - Are examples helpful and correct?
|
||||
|
||||
Format response as TOON:
|
||||
{{accuracy,completeness,clarity,examples,suggestions}}:
|
||||
good,complete,clear,helpful,None
|
||||
"""
|
||||
review = await self.think(prompt)
|
||||
ts = datetime.now(UTC).isoformat()
|
||||
ctx.notes.append(f"[{ts}] Reviewed {doc_spec.title}: {review[:100]}...")
|
||||
|
||||
async def _phase_publish(self, ctx: DocContext) -> None:
|
||||
"""
|
||||
PUBLISH phase: Documentation goes live.
|
||||
|
||||
- Write files to disk
|
||||
- Link docs to task
|
||||
- Update task status
|
||||
"""
|
||||
self.log.info("PUBLISH phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Write documentation files
|
||||
for doc_spec in ctx.documents_needed:
|
||||
if doc_spec.content:
|
||||
try:
|
||||
path = Path(doc_spec.path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
async with aiofiles.open(path, "w") as f:
|
||||
await f.write(doc_spec.content)
|
||||
self.log.info("Published", path=doc_spec.path)
|
||||
except Exception as e:
|
||||
self.log.error(
|
||||
"Failed to publish", path=doc_spec.path, error=str(e)
|
||||
)
|
||||
|
||||
# Use proper docs-complete endpoint (handles notes, status)
|
||||
doc_summary = f"Published: {', '.join(ctx.written_docs)}"
|
||||
await self._docs_complete(ctx.task_id, doc_summary)
|
||||
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"TASK-{str(ctx.task_id)[:8]} documentation complete, awaiting PM review\n"
|
||||
f"{doc_summary}",
|
||||
message_type="action",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
|
||||
ctx.notes.append(f"[{datetime.now(UTC).isoformat()}] Documentation published")
|
||||
|
||||
# =========================================================================
|
||||
# HELPER METHODS
|
||||
# =========================================================================
|
||||
|
||||
async def _find_awaiting_documentation(self) -> UUID | None:
|
||||
"""Find tasks awaiting documentation."""
|
||||
try:
|
||||
team_param = self.team.value if self.team else None
|
||||
result = await self._api_call(
|
||||
"GET",
|
||||
"/tasks",
|
||||
params={"status": "awaiting_documentation", "team": team_param},
|
||||
)
|
||||
tasks = result.get("items", [])
|
||||
return UUID(tasks[0]["id"]) if tasks else None
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to find awaiting documentation task", error=str(e))
|
||||
return None
|
||||
|
||||
async def _read_qa_feedback(self, task_id: UUID) -> str:
|
||||
"""Read QA feedback."""
|
||||
try:
|
||||
result = await self._api_call("GET", f"/tasks/{task_id}")
|
||||
feedback: str = result.get("qa_feedback", "No QA feedback available")
|
||||
return feedback
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to read QA feedback", error=str(e))
|
||||
return "QA feedback unavailable"
|
||||
|
||||
async def _get_conversations(self, task_id: UUID) -> list[str]:
|
||||
"""Get relevant conversations."""
|
||||
try:
|
||||
result = await self._api_call("GET", f"/tasks/{task_id}/messages")
|
||||
messages: list[dict[str, str]] = result.get("items", [])
|
||||
return [m.get("content", "") for m in messages]
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to get conversations", error=str(e))
|
||||
return []
|
||||
|
||||
async def _get_code_changes(self, task_id: UUID) -> list[str]:
|
||||
"""Get code changes from commits."""
|
||||
try:
|
||||
result = await self._api_call("GET", f"/tasks/{task_id}")
|
||||
changes: list[str] = result.get("code_changes", [])
|
||||
return changes
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to get code changes", error=str(e))
|
||||
return []
|
||||
@@ -1,92 +0,0 @@
|
||||
"""
|
||||
Agent Factories
|
||||
|
||||
Centralized factory functions for creating all agent types.
|
||||
|
||||
Modules:
|
||||
- developers: Developer agent factories
|
||||
- qa: QA agent factories
|
||||
- documenters: Documenter agent factories
|
||||
- pms: PM agent factories (Cell PMs and Main PM)
|
||||
- board: Board agent factories (Product Owner, Head Marketing, Auditor)
|
||||
- cells: Cell and Organization factories
|
||||
"""
|
||||
|
||||
# Board agents
|
||||
from roboco.agents.factories.board import (
|
||||
create_auditor,
|
||||
create_head_marketing,
|
||||
create_product_owner,
|
||||
)
|
||||
|
||||
# Cell and organization
|
||||
from roboco.agents.factories.cells import (
|
||||
create_backend_cell,
|
||||
create_board,
|
||||
create_frontend_cell,
|
||||
create_organization,
|
||||
create_ux_cell,
|
||||
get_agent_roster,
|
||||
print_org_chart,
|
||||
)
|
||||
|
||||
# Developers
|
||||
from roboco.agents.factories.developers import (
|
||||
create_backend_developer,
|
||||
create_frontend_developer,
|
||||
create_ux_developer,
|
||||
)
|
||||
|
||||
# Documenters
|
||||
from roboco.agents.factories.documenters import (
|
||||
create_backend_documenter,
|
||||
create_frontend_documenter,
|
||||
create_ux_documenter,
|
||||
)
|
||||
|
||||
# PMs
|
||||
from roboco.agents.factories.pms import (
|
||||
create_backend_pm,
|
||||
create_frontend_pm,
|
||||
create_main_pm,
|
||||
create_ux_pm,
|
||||
)
|
||||
|
||||
# QA
|
||||
from roboco.agents.factories.qa import (
|
||||
create_backend_qa,
|
||||
create_frontend_qa,
|
||||
create_ux_qa,
|
||||
)
|
||||
|
||||
# Organization types (re-exported for convenience)
|
||||
from roboco.models.organization import Board, Cell, Organization
|
||||
|
||||
__all__ = [
|
||||
"Board",
|
||||
"Cell",
|
||||
"Organization",
|
||||
"create_auditor",
|
||||
"create_backend_cell",
|
||||
"create_backend_developer",
|
||||
"create_backend_documenter",
|
||||
"create_backend_pm",
|
||||
"create_backend_qa",
|
||||
"create_board",
|
||||
"create_frontend_cell",
|
||||
"create_frontend_developer",
|
||||
"create_frontend_documenter",
|
||||
"create_frontend_pm",
|
||||
"create_frontend_qa",
|
||||
"create_head_marketing",
|
||||
"create_main_pm",
|
||||
"create_organization",
|
||||
"create_product_owner",
|
||||
"create_ux_cell",
|
||||
"create_ux_developer",
|
||||
"create_ux_documenter",
|
||||
"create_ux_pm",
|
||||
"create_ux_qa",
|
||||
"get_agent_roster",
|
||||
"print_org_chart",
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
"""
|
||||
Factory Base Utilities
|
||||
|
||||
Shared utilities for agent factory functions.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_blueprint_prompt(blueprint_path: str, default_prompt: str) -> str:
|
||||
"""
|
||||
Load system prompt from a blueprint file.
|
||||
|
||||
Args:
|
||||
blueprint_path: Relative path to the blueprint markdown file
|
||||
default_prompt: Default prompt if file doesn't exist or parsing fails
|
||||
|
||||
Returns:
|
||||
The extracted system prompt or the default
|
||||
"""
|
||||
path = Path(blueprint_path)
|
||||
if not path.exists():
|
||||
return default_prompt
|
||||
|
||||
content = path.read_text()
|
||||
# Extract system prompt section (between ```blocks after ## System Prompt)
|
||||
match = re.search(r"## System Prompt\s*```\s*(.*?)```", content, re.DOTALL)
|
||||
return match.group(1).strip() if match else default_prompt
|
||||
|
||||
|
||||
def make_slug(name: str) -> str:
|
||||
"""Convert a name to a URL-safe slug."""
|
||||
return name.lower().replace(" ", "-")
|
||||
@@ -1,83 +0,0 @@
|
||||
"""
|
||||
Board Agent Factories
|
||||
|
||||
Factory functions for creating board-level agents
|
||||
(Product Owner, Head of Marketing, Auditor).
|
||||
"""
|
||||
|
||||
from roboco.agents.board import AuditorAgent, HeadMarketingAgent, ProductOwnerAgent
|
||||
from roboco.agents.factories._base import load_blueprint_prompt
|
||||
from roboco.models import AgentRole, Team
|
||||
from roboco.models.agents import AgentConfig
|
||||
|
||||
|
||||
def create_product_owner(
|
||||
name: str = "Product Owner",
|
||||
system_prompt: str | None = None,
|
||||
) -> ProductOwnerAgent:
|
||||
"""Factory function to create the Product Owner agent."""
|
||||
if system_prompt is None:
|
||||
system_prompt = load_blueprint_prompt(
|
||||
"agents/blueprints/board/product-owner.md",
|
||||
"You are the Product Owner.",
|
||||
)
|
||||
|
||||
config = AgentConfig(
|
||||
name=name,
|
||||
slug="product-owner",
|
||||
role=AgentRole.PRODUCT_OWNER,
|
||||
team=Team.BOARD,
|
||||
system_prompt=system_prompt,
|
||||
capabilities=["requirements", "prioritization", "acceptance"],
|
||||
can_notify=True,
|
||||
)
|
||||
|
||||
return ProductOwnerAgent(config)
|
||||
|
||||
|
||||
def create_head_marketing(
|
||||
name: str = "Head of Marketing",
|
||||
system_prompt: str | None = None,
|
||||
) -> HeadMarketingAgent:
|
||||
"""Factory function to create the Head of Marketing agent."""
|
||||
if system_prompt is None:
|
||||
system_prompt = load_blueprint_prompt(
|
||||
"agents/blueprints/board/head-marketing.md",
|
||||
"You are the Head of Marketing.",
|
||||
)
|
||||
|
||||
config = AgentConfig(
|
||||
name=name,
|
||||
slug="head-marketing",
|
||||
role=AgentRole.HEAD_MARKETING,
|
||||
team=Team.BOARD,
|
||||
system_prompt=system_prompt,
|
||||
capabilities=["marketing", "campaigns", "analytics"],
|
||||
can_notify=True,
|
||||
)
|
||||
|
||||
return HeadMarketingAgent(config)
|
||||
|
||||
|
||||
def create_auditor(
|
||||
name: str = "Auditor",
|
||||
system_prompt: str | None = None,
|
||||
) -> AuditorAgent:
|
||||
"""Factory function to create the Auditor agent."""
|
||||
if system_prompt is None:
|
||||
system_prompt = load_blueprint_prompt(
|
||||
"agents/blueprints/board/auditor.md",
|
||||
"You are the Auditor - the CEO's silent ally.",
|
||||
)
|
||||
|
||||
config = AgentConfig(
|
||||
name=name,
|
||||
slug="auditor",
|
||||
role=AgentRole.AUDITOR,
|
||||
team=Team.BOARD,
|
||||
system_prompt=system_prompt,
|
||||
capabilities=["observation", "analysis", "audit", "ceo_reporting"],
|
||||
can_notify=True,
|
||||
)
|
||||
|
||||
return AuditorAgent(config)
|
||||
@@ -1,217 +0,0 @@
|
||||
"""
|
||||
Cell and Organization Factories
|
||||
|
||||
Factory functions for creating complete cells and the full organization.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from roboco.agents.factories.board import (
|
||||
create_auditor,
|
||||
create_head_marketing,
|
||||
create_product_owner,
|
||||
)
|
||||
from roboco.agents.factories.developers import (
|
||||
create_backend_developer,
|
||||
create_frontend_developer,
|
||||
create_ux_developer,
|
||||
)
|
||||
from roboco.agents.factories.documenters import (
|
||||
create_backend_documenter,
|
||||
create_frontend_documenter,
|
||||
create_ux_documenter,
|
||||
)
|
||||
from roboco.agents.factories.pms import (
|
||||
create_backend_pm,
|
||||
create_frontend_pm,
|
||||
create_main_pm,
|
||||
create_ux_pm,
|
||||
)
|
||||
from roboco.agents.factories.qa import (
|
||||
create_backend_qa,
|
||||
create_frontend_qa,
|
||||
create_ux_qa,
|
||||
)
|
||||
from roboco.models import Team
|
||||
from roboco.models.organization import Board, Cell, Organization
|
||||
|
||||
|
||||
def create_backend_cell() -> Cell:
|
||||
"""
|
||||
Create a complete Backend cell.
|
||||
|
||||
Includes:
|
||||
- 1 PM (BE-PM)
|
||||
- 2 Developers (BE-Dev-1, BE-Dev-2)
|
||||
- 1 QA (BE-QA)
|
||||
- 1 Documenter (BE-Documenter)
|
||||
"""
|
||||
return Cell(
|
||||
name="backend-cell",
|
||||
team=Team.BACKEND,
|
||||
pm=create_backend_pm(),
|
||||
developers=[
|
||||
create_backend_developer("BE-Dev-1"),
|
||||
create_backend_developer("BE-Dev-2"),
|
||||
],
|
||||
qa=create_backend_qa(),
|
||||
documenter=create_backend_documenter(),
|
||||
)
|
||||
|
||||
|
||||
def create_frontend_cell() -> Cell:
|
||||
"""
|
||||
Create a complete Frontend cell.
|
||||
|
||||
Includes:
|
||||
- 1 PM (FE-PM)
|
||||
- 2 Developers (FE-Dev-1, FE-Dev-2)
|
||||
- 1 QA (FE-QA)
|
||||
- 1 Documenter (FE-Documenter)
|
||||
"""
|
||||
return Cell(
|
||||
name="frontend-cell",
|
||||
team=Team.FRONTEND,
|
||||
pm=create_frontend_pm(),
|
||||
developers=[
|
||||
create_frontend_developer("FE-Dev-1"),
|
||||
create_frontend_developer("FE-Dev-2"),
|
||||
],
|
||||
qa=create_frontend_qa(),
|
||||
documenter=create_frontend_documenter(),
|
||||
)
|
||||
|
||||
|
||||
def create_ux_cell() -> Cell:
|
||||
"""
|
||||
Create a complete UX/UI cell.
|
||||
|
||||
Includes:
|
||||
- 1 PM (UX-PM)
|
||||
- 1 Developer (UX-Dev)
|
||||
- 1 QA (UX-QA)
|
||||
- 1 Documenter (UX-Documenter)
|
||||
"""
|
||||
return Cell(
|
||||
name="uxui-cell",
|
||||
team=Team.UX_UI,
|
||||
pm=create_ux_pm(),
|
||||
developers=[
|
||||
create_ux_developer("UX-Dev"),
|
||||
],
|
||||
qa=create_ux_qa(),
|
||||
documenter=create_ux_documenter(),
|
||||
)
|
||||
|
||||
|
||||
def create_board() -> Board:
|
||||
"""
|
||||
Create the Board level.
|
||||
|
||||
Includes:
|
||||
- Product Owner
|
||||
- Head of Marketing
|
||||
- Auditor
|
||||
"""
|
||||
return Board(
|
||||
product_owner=create_product_owner(),
|
||||
head_marketing=create_head_marketing(),
|
||||
auditor=create_auditor(),
|
||||
)
|
||||
|
||||
|
||||
def create_organization() -> Organization:
|
||||
"""
|
||||
Create the complete AI organization.
|
||||
|
||||
Total: 18 AI agents
|
||||
- Board: 3 (Product Owner, Head of Marketing, Auditor)
|
||||
- Management: 1 (Main PM)
|
||||
- Backend Cell: 5 (PM, 2 Devs, QA, Documenter)
|
||||
- Frontend Cell: 5 (PM, 2 Devs, QA, Documenter)
|
||||
- UX/UI Cell: 4 (PM, 1 Dev, QA, Documenter)
|
||||
"""
|
||||
return Organization(
|
||||
board=create_board(),
|
||||
main_pm=create_main_pm(),
|
||||
backend_cell=create_backend_cell(),
|
||||
frontend_cell=create_frontend_cell(),
|
||||
ux_cell=create_ux_cell(),
|
||||
)
|
||||
|
||||
|
||||
def get_agent_roster() -> dict[str, list[dict[str, Any]]]:
|
||||
"""
|
||||
Get a roster of all agents without instantiating them.
|
||||
|
||||
Useful for displaying the org structure.
|
||||
"""
|
||||
return {
|
||||
"board": [
|
||||
{"name": "Product Owner", "role": "product_owner", "slug": "product-owner"},
|
||||
{
|
||||
"name": "Head of Marketing",
|
||||
"role": "head_marketing",
|
||||
"slug": "head-marketing",
|
||||
},
|
||||
{"name": "Auditor", "role": "auditor", "slug": "auditor"},
|
||||
],
|
||||
"management": [
|
||||
{"name": "Main PM", "role": "main_pm", "slug": "main-pm"},
|
||||
],
|
||||
"backend_cell": [
|
||||
{"name": "BE-PM", "role": "cell_pm", "slug": "be-pm"},
|
||||
{"name": "BE-Dev-1", "role": "developer", "slug": "be-dev-1"},
|
||||
{"name": "BE-Dev-2", "role": "developer", "slug": "be-dev-2"},
|
||||
{"name": "BE-QA", "role": "qa", "slug": "be-qa"},
|
||||
{"name": "BE-Documenter", "role": "documenter", "slug": "be-documenter"},
|
||||
],
|
||||
"frontend_cell": [
|
||||
{"name": "FE-PM", "role": "cell_pm", "slug": "fe-pm"},
|
||||
{"name": "FE-Dev-1", "role": "developer", "slug": "fe-dev-1"},
|
||||
{"name": "FE-Dev-2", "role": "developer", "slug": "fe-dev-2"},
|
||||
{"name": "FE-QA", "role": "qa", "slug": "fe-qa"},
|
||||
{"name": "FE-Documenter", "role": "documenter", "slug": "fe-documenter"},
|
||||
],
|
||||
"ux_cell": [
|
||||
{"name": "UX-PM", "role": "cell_pm", "slug": "ux-pm"},
|
||||
{"name": "UX-Dev", "role": "developer", "slug": "ux-dev"},
|
||||
{"name": "UX-QA", "role": "qa", "slug": "ux-qa"},
|
||||
{"name": "UX-Documenter", "role": "documenter", "slug": "ux-documenter"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def print_org_chart() -> str:
|
||||
"""Generate a text-based org chart."""
|
||||
return """
|
||||
┌─────────────┐
|
||||
│ CEO │
|
||||
│ (Human) │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌────────────────┼────────────────┐
|
||||
│ │ │
|
||||
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
|
||||
│ Product │ │ Head │ │ Auditor │
|
||||
│ Owner │ │ Marketing │ │ (Spy) │
|
||||
└─────┬─────┘ └─────┬─────┘ └───────────┘
|
||||
│ │ ▲
|
||||
└───────┬────────┘ │
|
||||
│ [observes all]
|
||||
┌──────▼──────┐
|
||||
│ Main PM │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
|
||||
│ BE-PM │ │ FE-PM │ │ UX-PM │
|
||||
├───────────┤ ├───────────┤ ├───────────┤
|
||||
│ BE-Dev x2 │ │ FE-Dev x2 │ │ UX-Dev │
|
||||
│ BE-QA │ │ FE-QA │ │ UX-QA │
|
||||
│ BE-Doc │ │ FE-Doc │ │ UX-Doc │
|
||||
└───────────┘ └───────────┘ └───────────┘
|
||||
|
||||
Total: 18 AI Agents + 1 Human CEO = 19 organization members
|
||||
"""
|
||||
@@ -1,109 +0,0 @@
|
||||
"""
|
||||
Developer Agent Factories
|
||||
|
||||
Factory functions for creating developer agents for each team.
|
||||
"""
|
||||
|
||||
from roboco.agents.developer import DeveloperAgent
|
||||
from roboco.agents.factories._base import load_blueprint_prompt, make_slug
|
||||
from roboco.models import AgentRole, Team
|
||||
from roboco.models.agents import AgentConfig
|
||||
|
||||
# Blueprint paths for each team
|
||||
_BLUEPRINTS = {
|
||||
Team.BACKEND: "agents/blueprints/backend/be-dev.md",
|
||||
Team.FRONTEND: "agents/blueprints/frontend/fe-dev.md",
|
||||
Team.UX_UI: "agents/blueprints/ux_ui/ux-dev.md",
|
||||
}
|
||||
|
||||
# Default prompts for each team
|
||||
_DEFAULT_PROMPTS = {
|
||||
Team.BACKEND: "You are a backend developer.",
|
||||
Team.FRONTEND: "You are a frontend developer.",
|
||||
Team.UX_UI: "You are a UX/UI developer.",
|
||||
}
|
||||
|
||||
# Default capabilities for each team (matches blueprint capabilities)
|
||||
_CAPABILITIES = {
|
||||
Team.BACKEND: [
|
||||
"code_execution",
|
||||
"git_operations",
|
||||
"file_management",
|
||||
"api_development",
|
||||
"database_design",
|
||||
],
|
||||
Team.FRONTEND: [
|
||||
"code_execution",
|
||||
"git_operations",
|
||||
"file_management",
|
||||
"browser_testing",
|
||||
"accessibility_testing",
|
||||
"responsive_design",
|
||||
],
|
||||
Team.UX_UI: [
|
||||
"design_tools",
|
||||
"file_management",
|
||||
"figma_expertise",
|
||||
"prototyping",
|
||||
"design_system_management",
|
||||
"accessibility_design",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _create_developer(
|
||||
name: str,
|
||||
team: Team,
|
||||
system_prompt: str | None = None,
|
||||
) -> DeveloperAgent:
|
||||
"""
|
||||
Internal factory for creating a developer agent.
|
||||
|
||||
Args:
|
||||
name: Agent display name
|
||||
team: Team assignment
|
||||
system_prompt: Optional custom system prompt
|
||||
|
||||
Returns:
|
||||
Configured DeveloperAgent instance
|
||||
"""
|
||||
if system_prompt is None:
|
||||
system_prompt = load_blueprint_prompt(
|
||||
_BLUEPRINTS[team],
|
||||
_DEFAULT_PROMPTS[team],
|
||||
)
|
||||
|
||||
config = AgentConfig(
|
||||
name=name,
|
||||
slug=make_slug(name),
|
||||
role=AgentRole.DEVELOPER,
|
||||
team=team,
|
||||
system_prompt=system_prompt,
|
||||
capabilities=_CAPABILITIES[team],
|
||||
)
|
||||
|
||||
return DeveloperAgent(config)
|
||||
|
||||
|
||||
def create_backend_developer(
|
||||
name: str = "BE-Dev-1",
|
||||
system_prompt: str | None = None,
|
||||
) -> DeveloperAgent:
|
||||
"""Factory function to create a backend developer agent."""
|
||||
return _create_developer(name, Team.BACKEND, system_prompt)
|
||||
|
||||
|
||||
def create_frontend_developer(
|
||||
name: str = "FE-Dev-1",
|
||||
system_prompt: str | None = None,
|
||||
) -> DeveloperAgent:
|
||||
"""Factory function to create a frontend developer agent."""
|
||||
return _create_developer(name, Team.FRONTEND, system_prompt)
|
||||
|
||||
|
||||
def create_ux_developer(
|
||||
name: str = "UX-Dev-1",
|
||||
system_prompt: str | None = None,
|
||||
) -> DeveloperAgent:
|
||||
"""Factory function to create a UX/UI developer agent."""
|
||||
return _create_developer(name, Team.UX_UI, system_prompt)
|
||||
@@ -1,105 +0,0 @@
|
||||
"""
|
||||
Documenter Agent Factories
|
||||
|
||||
Factory functions for creating documenter agents for each team.
|
||||
"""
|
||||
|
||||
from roboco.agents.documenter import DocumenterAgent
|
||||
from roboco.agents.factories._base import load_blueprint_prompt, make_slug
|
||||
from roboco.models import AgentRole, Team
|
||||
from roboco.models.agents import AgentConfig
|
||||
|
||||
# Blueprint paths for each team
|
||||
_BLUEPRINTS = {
|
||||
Team.BACKEND: "agents/blueprints/backend/be-documenter.md",
|
||||
Team.FRONTEND: "agents/blueprints/frontend/fe-documenter.md",
|
||||
Team.UX_UI: "agents/blueprints/ux_ui/ux-documenter.md",
|
||||
}
|
||||
|
||||
# Default prompts for each team
|
||||
_DEFAULT_PROMPTS = {
|
||||
Team.BACKEND: "You are a backend documenter.",
|
||||
Team.FRONTEND: "You are a frontend documenter.",
|
||||
Team.UX_UI: "You are a UX/UI documenter.",
|
||||
}
|
||||
|
||||
# Default capabilities for each team (matches blueprint capabilities)
|
||||
_CAPABILITIES = {
|
||||
Team.BACKEND: [
|
||||
"technical_writing",
|
||||
"api_documentation",
|
||||
"code_reading",
|
||||
"file_management",
|
||||
],
|
||||
Team.FRONTEND: [
|
||||
"technical_writing",
|
||||
"component_documentation",
|
||||
"code_reading",
|
||||
"storybook",
|
||||
"file_management",
|
||||
],
|
||||
Team.UX_UI: [
|
||||
"design_documentation",
|
||||
"design_system_maintenance",
|
||||
"technical_writing",
|
||||
"file_management",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _create_documenter(
|
||||
name: str,
|
||||
team: Team,
|
||||
system_prompt: str | None = None,
|
||||
) -> DocumenterAgent:
|
||||
"""
|
||||
Internal factory for creating a documenter agent.
|
||||
|
||||
Args:
|
||||
name: Agent display name
|
||||
team: Team assignment
|
||||
system_prompt: Optional custom system prompt
|
||||
|
||||
Returns:
|
||||
Configured DocumenterAgent instance
|
||||
"""
|
||||
if system_prompt is None:
|
||||
system_prompt = load_blueprint_prompt(
|
||||
_BLUEPRINTS[team],
|
||||
_DEFAULT_PROMPTS[team],
|
||||
)
|
||||
|
||||
config = AgentConfig(
|
||||
name=name,
|
||||
slug=make_slug(name),
|
||||
role=AgentRole.DOCUMENTER,
|
||||
team=team,
|
||||
system_prompt=system_prompt,
|
||||
capabilities=_CAPABILITIES[team],
|
||||
)
|
||||
|
||||
return DocumenterAgent(config)
|
||||
|
||||
|
||||
def create_backend_documenter(
|
||||
name: str = "BE-Documenter",
|
||||
system_prompt: str | None = None,
|
||||
) -> DocumenterAgent:
|
||||
"""Factory function to create a backend documenter agent."""
|
||||
return _create_documenter(name, Team.BACKEND, system_prompt)
|
||||
|
||||
|
||||
def create_frontend_documenter(
|
||||
name: str = "FE-Documenter",
|
||||
system_prompt: str | None = None,
|
||||
) -> DocumenterAgent:
|
||||
"""Factory function to create a frontend documenter agent."""
|
||||
return _create_documenter(name, Team.FRONTEND, system_prompt)
|
||||
|
||||
|
||||
def create_ux_documenter(
|
||||
name: str = "UX-Documenter",
|
||||
system_prompt: str | None = None,
|
||||
) -> DocumenterAgent:
|
||||
"""Factory function to create a UX/UI documenter agent."""
|
||||
return _create_documenter(name, Team.UX_UI, system_prompt)
|
||||
@@ -1,107 +0,0 @@
|
||||
"""
|
||||
PM Agent Factories
|
||||
|
||||
Factory functions for creating PM agents (Cell PMs and Main PM).
|
||||
"""
|
||||
|
||||
from roboco.agents.factories._base import load_blueprint_prompt, make_slug
|
||||
from roboco.agents.pm import CellPMAgent, MainPMAgent
|
||||
from roboco.models import AgentRole, Team
|
||||
from roboco.models.agents import AgentConfig
|
||||
|
||||
# Blueprint paths for cell PMs
|
||||
_CELL_PM_BLUEPRINTS = {
|
||||
Team.BACKEND: "agents/blueprints/backend/be-pm.md",
|
||||
Team.FRONTEND: "agents/blueprints/frontend/fe-pm.md",
|
||||
Team.UX_UI: "agents/blueprints/ux_ui/ux-pm.md",
|
||||
}
|
||||
|
||||
# Default prompts for cell PMs
|
||||
_CELL_PM_PROMPTS = {
|
||||
Team.BACKEND: "You are the Backend Cell PM.",
|
||||
Team.FRONTEND: "You are the Frontend Cell PM.",
|
||||
Team.UX_UI: "You are the UX/UI Cell PM.",
|
||||
}
|
||||
|
||||
|
||||
def _create_cell_pm(
|
||||
name: str,
|
||||
team: Team,
|
||||
system_prompt: str | None = None,
|
||||
) -> CellPMAgent:
|
||||
"""
|
||||
Internal factory for creating a cell PM agent.
|
||||
|
||||
Args:
|
||||
name: Agent display name
|
||||
team: Team assignment
|
||||
system_prompt: Optional custom system prompt
|
||||
|
||||
Returns:
|
||||
Configured CellPMAgent instance
|
||||
"""
|
||||
if system_prompt is None:
|
||||
system_prompt = load_blueprint_prompt(
|
||||
_CELL_PM_BLUEPRINTS[team],
|
||||
_CELL_PM_PROMPTS[team],
|
||||
)
|
||||
|
||||
config = AgentConfig(
|
||||
name=name,
|
||||
slug=make_slug(name),
|
||||
role=AgentRole.CELL_PM,
|
||||
team=team,
|
||||
system_prompt=system_prompt,
|
||||
capabilities=["task_management", "notifications"],
|
||||
can_notify=True,
|
||||
)
|
||||
|
||||
return CellPMAgent(config)
|
||||
|
||||
|
||||
def create_backend_pm(
|
||||
name: str = "BE-PM",
|
||||
system_prompt: str | None = None,
|
||||
) -> CellPMAgent:
|
||||
"""Factory function to create a backend PM agent."""
|
||||
return _create_cell_pm(name, Team.BACKEND, system_prompt)
|
||||
|
||||
|
||||
def create_frontend_pm(
|
||||
name: str = "FE-PM",
|
||||
system_prompt: str | None = None,
|
||||
) -> CellPMAgent:
|
||||
"""Factory function to create a frontend PM agent."""
|
||||
return _create_cell_pm(name, Team.FRONTEND, system_prompt)
|
||||
|
||||
|
||||
def create_ux_pm(
|
||||
name: str = "UX-PM",
|
||||
system_prompt: str | None = None,
|
||||
) -> CellPMAgent:
|
||||
"""Factory function to create a UX/UI PM agent."""
|
||||
return _create_cell_pm(name, Team.UX_UI, system_prompt)
|
||||
|
||||
|
||||
def create_main_pm(
|
||||
name: str = "Main PM",
|
||||
system_prompt: str | None = None,
|
||||
) -> MainPMAgent:
|
||||
"""Factory function to create the Main PM agent."""
|
||||
if system_prompt is None:
|
||||
system_prompt = load_blueprint_prompt(
|
||||
"agents/blueprints/board/main-pm.md",
|
||||
"You are the Main PM coordinating all cells.",
|
||||
)
|
||||
|
||||
config = AgentConfig(
|
||||
name=name,
|
||||
slug="main-pm",
|
||||
role=AgentRole.MAIN_PM,
|
||||
team=Team.BOARD,
|
||||
system_prompt=system_prompt,
|
||||
capabilities=["task_management", "notifications", "cross_cell_coordination"],
|
||||
can_notify=True,
|
||||
)
|
||||
|
||||
return MainPMAgent(config)
|
||||
@@ -1,103 +0,0 @@
|
||||
"""
|
||||
QA Agent Factories
|
||||
|
||||
Factory functions for creating QA agents for each team.
|
||||
"""
|
||||
|
||||
from roboco.agents.factories._base import load_blueprint_prompt, make_slug
|
||||
from roboco.agents.qa import QAAgent
|
||||
from roboco.models import AgentRole, Team
|
||||
from roboco.models.agents import AgentConfig
|
||||
|
||||
# Blueprint paths for each team
|
||||
_BLUEPRINTS = {
|
||||
Team.BACKEND: "agents/blueprints/backend/be-qa.md",
|
||||
Team.FRONTEND: "agents/blueprints/frontend/fe-qa.md",
|
||||
Team.UX_UI: "agents/blueprints/ux_ui/ux-qa.md",
|
||||
}
|
||||
|
||||
# Default prompts for each team
|
||||
_DEFAULT_PROMPTS = {
|
||||
Team.BACKEND: "You are a backend QA engineer.",
|
||||
Team.FRONTEND: "You are a frontend QA engineer.",
|
||||
Team.UX_UI: "You are a UX/UI QA engineer.",
|
||||
}
|
||||
|
||||
# Default capabilities for each team (matches blueprint capabilities)
|
||||
_CAPABILITIES = {
|
||||
Team.BACKEND: [
|
||||
"code_review",
|
||||
"test_execution",
|
||||
"security_analysis",
|
||||
"quality_assurance",
|
||||
],
|
||||
Team.FRONTEND: [
|
||||
"visual_testing",
|
||||
"accessibility_testing",
|
||||
"browser_testing",
|
||||
"quality_assurance",
|
||||
],
|
||||
Team.UX_UI: [
|
||||
"design_review",
|
||||
"accessibility_review",
|
||||
"quality_assurance",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _create_qa(
|
||||
name: str,
|
||||
team: Team,
|
||||
system_prompt: str | None = None,
|
||||
) -> QAAgent:
|
||||
"""
|
||||
Internal factory for creating a QA agent.
|
||||
|
||||
Args:
|
||||
name: Agent display name
|
||||
team: Team assignment
|
||||
system_prompt: Optional custom system prompt
|
||||
|
||||
Returns:
|
||||
Configured QAAgent instance
|
||||
"""
|
||||
if system_prompt is None:
|
||||
system_prompt = load_blueprint_prompt(
|
||||
_BLUEPRINTS[team],
|
||||
_DEFAULT_PROMPTS[team],
|
||||
)
|
||||
|
||||
config = AgentConfig(
|
||||
name=name,
|
||||
slug=make_slug(name),
|
||||
role=AgentRole.QA,
|
||||
team=team,
|
||||
system_prompt=system_prompt,
|
||||
capabilities=_CAPABILITIES[team],
|
||||
)
|
||||
|
||||
return QAAgent(config)
|
||||
|
||||
|
||||
def create_backend_qa(
|
||||
name: str = "BE-QA",
|
||||
system_prompt: str | None = None,
|
||||
) -> QAAgent:
|
||||
"""Factory function to create a backend QA agent."""
|
||||
return _create_qa(name, Team.BACKEND, system_prompt)
|
||||
|
||||
|
||||
def create_frontend_qa(
|
||||
name: str = "FE-QA",
|
||||
system_prompt: str | None = None,
|
||||
) -> QAAgent:
|
||||
"""Factory function to create a frontend QA agent."""
|
||||
return _create_qa(name, Team.FRONTEND, system_prompt)
|
||||
|
||||
|
||||
def create_ux_qa(
|
||||
name: str = "UX-QA",
|
||||
system_prompt: str | None = None,
|
||||
) -> QAAgent:
|
||||
"""Factory function to create a UX/UI QA agent."""
|
||||
return _create_qa(name, Team.UX_UI, system_prompt)
|
||||
@@ -1,59 +0,0 @@
|
||||
"""
|
||||
Agent Factory (Backwards Compatibility)
|
||||
|
||||
This module re-exports all factories from the new location.
|
||||
Use roboco.agents.factories instead for new code.
|
||||
"""
|
||||
|
||||
# Re-export everything from the new factories package
|
||||
from roboco.agents.factories import (
|
||||
create_auditor,
|
||||
create_backend_cell,
|
||||
create_backend_developer,
|
||||
create_backend_documenter,
|
||||
create_backend_pm,
|
||||
create_backend_qa,
|
||||
create_board,
|
||||
create_frontend_cell,
|
||||
create_frontend_developer,
|
||||
create_frontend_documenter,
|
||||
create_frontend_pm,
|
||||
create_frontend_qa,
|
||||
create_head_marketing,
|
||||
create_main_pm,
|
||||
create_organization,
|
||||
create_product_owner,
|
||||
create_ux_cell,
|
||||
create_ux_developer,
|
||||
create_ux_documenter,
|
||||
create_ux_pm,
|
||||
create_ux_qa,
|
||||
get_agent_roster,
|
||||
print_org_chart,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"create_auditor",
|
||||
"create_backend_cell",
|
||||
"create_backend_developer",
|
||||
"create_backend_documenter",
|
||||
"create_backend_pm",
|
||||
"create_backend_qa",
|
||||
"create_board",
|
||||
"create_frontend_cell",
|
||||
"create_frontend_developer",
|
||||
"create_frontend_documenter",
|
||||
"create_frontend_pm",
|
||||
"create_frontend_qa",
|
||||
"create_head_marketing",
|
||||
"create_main_pm",
|
||||
"create_organization",
|
||||
"create_product_owner",
|
||||
"create_ux_cell",
|
||||
"create_ux_developer",
|
||||
"create_ux_documenter",
|
||||
"create_ux_pm",
|
||||
"create_ux_qa",
|
||||
"get_agent_roster",
|
||||
"print_org_chart",
|
||||
]
|
||||
@@ -1,415 +0,0 @@
|
||||
"""
|
||||
Agent Mixins and Abstractions
|
||||
|
||||
Reusable components for agent implementations:
|
||||
- PhaseEngine: Unified phase dispatch and transitions
|
||||
- ContextManager: Context lifecycle management
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
# =============================================================================
|
||||
# PHASE ENGINE
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhaseConfig[PhaseT: Enum]:
|
||||
"""
|
||||
Configuration for a phase in the workflow.
|
||||
|
||||
Attributes:
|
||||
phase: The phase enum value
|
||||
handler: Async function to execute for this phase
|
||||
next_phase: Phase to transition to after completion (None = terminal)
|
||||
fail_phase: Phase to transition to if handler returns False (optional)
|
||||
requires_completion: If True, handler must return True to advance
|
||||
"""
|
||||
|
||||
phase: PhaseT
|
||||
handler: Callable[..., Awaitable[bool | None]]
|
||||
next_phase: PhaseT | None = None
|
||||
fail_phase: PhaseT | None = None
|
||||
requires_completion: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhaseResult:
|
||||
"""Result of running a phase."""
|
||||
|
||||
completed: bool = False # True if workflow complete
|
||||
advanced: bool = False # True if phase advanced
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class PhaseEngine[PhaseT: Enum, ContextT]:
|
||||
"""
|
||||
Mixin for phase-based workflow execution.
|
||||
|
||||
Provides unified phase dispatch and transition logic that can be
|
||||
configured per agent type. Replaces duplicate _dispatch_phase and
|
||||
_run_phase implementations across agents.
|
||||
|
||||
Usage:
|
||||
class MyAgent(Agent, PhaseEngine[MyPhase, MyContext]):
|
||||
def _get_phase_configs(self) -> list[PhaseConfig[MyPhase]]:
|
||||
return [
|
||||
PhaseConfig(MyPhase.START, self._phase_start, MyPhase.WORK),
|
||||
PhaseConfig(MyPhase.WORK, self._phase_work, MyPhase.END,
|
||||
requires_completion=True),
|
||||
PhaseConfig(MyPhase.END, self._phase_end, None),
|
||||
]
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def _get_phase_configs(self) -> list[PhaseConfig[PhaseT]]:
|
||||
"""
|
||||
Define the phase workflow configuration.
|
||||
|
||||
Returns:
|
||||
List of PhaseConfig defining handlers and transitions.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def _get_current_phase(self, ctx: ContextT) -> PhaseT:
|
||||
"""Get the current phase from context."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def _set_current_phase(self, ctx: ContextT, phase: PhaseT) -> None:
|
||||
"""Set the current phase in context."""
|
||||
...
|
||||
|
||||
async def _run_phase_engine(self, ctx: ContextT) -> PhaseResult:
|
||||
"""
|
||||
Execute the current phase and handle transitions.
|
||||
|
||||
Args:
|
||||
ctx: The workflow context
|
||||
|
||||
Returns:
|
||||
PhaseResult indicating completion status
|
||||
"""
|
||||
configs = {cfg.phase: cfg for cfg in self._get_phase_configs()}
|
||||
current_phase = self._get_current_phase(ctx)
|
||||
|
||||
config = configs.get(current_phase)
|
||||
if not config:
|
||||
return PhaseResult(error=f"No config for phase: {current_phase}")
|
||||
|
||||
try:
|
||||
# Execute handler
|
||||
result = await config.handler(ctx)
|
||||
|
||||
# Terminal phase check
|
||||
if config.next_phase is None:
|
||||
return PhaseResult(completed=True)
|
||||
|
||||
# Check if we should advance
|
||||
should_advance = True
|
||||
if config.requires_completion:
|
||||
should_advance = result is True
|
||||
|
||||
if should_advance:
|
||||
self._set_current_phase(ctx, config.next_phase)
|
||||
return PhaseResult(advanced=True)
|
||||
|
||||
# Handle failure transition (e.g., VERIFY fails → back to EXECUTE)
|
||||
if config.fail_phase is not None and result is False:
|
||||
self._set_current_phase(ctx, config.fail_phase)
|
||||
return PhaseResult(advanced=True)
|
||||
|
||||
return PhaseResult()
|
||||
|
||||
except Exception as e:
|
||||
return PhaseResult(error=str(e))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CYCLIC PHASE RUNNER (for continuous-duty agents like PM/Board)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class CyclicPhaseConfig[PhaseT: Enum]:
|
||||
"""
|
||||
Configuration for a phase in a continuous cycle.
|
||||
|
||||
Unlike PhaseConfig, this is for agents that cycle forever
|
||||
(PM, Board agents) rather than completing tasks.
|
||||
"""
|
||||
|
||||
phase: PhaseT
|
||||
handler: Callable[..., Awaitable[None]]
|
||||
next_phase: PhaseT
|
||||
|
||||
|
||||
class CyclicPhaseRunner[PhaseT: Enum]:
|
||||
"""
|
||||
Mixin for continuous-duty agents that cycle through phases.
|
||||
|
||||
Unlike PhaseEngine which handles task completion, this is for
|
||||
agents like PM and Board that run continuously.
|
||||
|
||||
Usage:
|
||||
class CellPMAgent(Agent, CyclicPhaseRunner[CellPMPhase]):
|
||||
_current_phase: CellPMPhase = CellPMPhase.MONITOR
|
||||
|
||||
def _get_cyclic_phase_configs(self) -> list[CyclicPhaseConfig]:
|
||||
return [
|
||||
CyclicPhaseConfig(CellPMPhase.MONITOR, self._phase_monitor,
|
||||
CellPMPhase.TRIAGE),
|
||||
# ... etc
|
||||
]
|
||||
"""
|
||||
|
||||
_current_phase: PhaseT
|
||||
|
||||
@abstractmethod
|
||||
def _get_cyclic_phase_configs(self) -> list[CyclicPhaseConfig[PhaseT]]:
|
||||
"""Define the cyclic phase workflow."""
|
||||
...
|
||||
|
||||
async def _run_phase_cycle(self) -> str | None:
|
||||
"""
|
||||
Execute the current phase and advance to next.
|
||||
|
||||
Returns error message if any, None on success.
|
||||
"""
|
||||
configs = {cfg.phase: cfg for cfg in self._get_cyclic_phase_configs()}
|
||||
|
||||
config = configs.get(self._current_phase)
|
||||
if not config:
|
||||
return f"No config for phase: {self._current_phase}"
|
||||
|
||||
try:
|
||||
await config.handler()
|
||||
self._current_phase = config.next_phase
|
||||
return None
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONTEXT MANAGER
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseContext:
|
||||
"""Base context with common fields."""
|
||||
|
||||
task_id: UUID
|
||||
title: str = ""
|
||||
notes: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class ContextManager[ContextT]:
|
||||
"""
|
||||
Mixin for managing workflow context lifecycle.
|
||||
|
||||
Handles:
|
||||
- Context initialization/restoration
|
||||
- Context cleanup on completion
|
||||
- Type-safe context access
|
||||
|
||||
Usage:
|
||||
class MyAgent(Agent, ContextManager[MyContext]):
|
||||
_context: MyContext | None = None
|
||||
|
||||
def _create_context(self, task_id: UUID, title: str) -> MyContext:
|
||||
return MyContext(task_id=task_id, title=title)
|
||||
|
||||
async def execute_task(self, task_id: UUID) -> bool:
|
||||
ctx = await self._ensure_context(task_id)
|
||||
# ... work with ctx
|
||||
"""
|
||||
|
||||
_context: ContextT | None
|
||||
|
||||
@abstractmethod
|
||||
def _create_context(self, task_id: UUID, title: str) -> ContextT:
|
||||
"""Create a new context instance."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def _get_context_task_id(self, ctx: ContextT) -> UUID:
|
||||
"""Get the task ID from a context."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def _get_task_title(self, task_id: UUID) -> str:
|
||||
"""Get task title from API (implemented in base Agent)."""
|
||||
...
|
||||
|
||||
async def _ensure_context(self, task_id: UUID) -> ContextT:
|
||||
"""
|
||||
Ensure context exists for the given task.
|
||||
|
||||
Creates new context if none exists or if task ID changed.
|
||||
"""
|
||||
if self._context is None or self._get_context_task_id(self._context) != task_id:
|
||||
title = await self._get_task_title(task_id)
|
||||
self._context = self._create_context(task_id, title)
|
||||
return self._context
|
||||
|
||||
def _clear_context(self) -> None:
|
||||
"""Clear the current context."""
|
||||
self._context = None
|
||||
|
||||
@property
|
||||
def context(self) -> ContextT | None:
|
||||
"""Get the current context (may be None)."""
|
||||
return self._context
|
||||
|
||||
def require_context(self) -> ContextT:
|
||||
"""Get context, raising if None."""
|
||||
if self._context is None:
|
||||
raise RuntimeError("No active context")
|
||||
return self._context
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# WORK FINDER
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkSearchStrategy:
|
||||
"""
|
||||
A strategy for finding work.
|
||||
|
||||
Attributes:
|
||||
name: Descriptive name for logging
|
||||
finder: Async function that returns task ID or None
|
||||
priority: Lower = higher priority
|
||||
"""
|
||||
|
||||
name: str
|
||||
finder: Callable[[], Awaitable[UUID | None]]
|
||||
priority: int = 0
|
||||
|
||||
|
||||
class WorkFinder:
|
||||
"""
|
||||
Mixin for finding work with prioritized strategies.
|
||||
|
||||
Replaces duplicate find_work implementations with a configurable
|
||||
search strategy pattern.
|
||||
|
||||
Usage:
|
||||
class MyAgent(Agent, WorkFinder):
|
||||
def _get_work_strategies(self) -> list[WorkSearchStrategy]:
|
||||
return [
|
||||
WorkSearchStrategy("paused", self._find_paused, priority=0),
|
||||
WorkSearchStrategy("assigned", self._find_assigned, priority=1),
|
||||
]
|
||||
"""
|
||||
|
||||
_pending_work: list[UUID]
|
||||
|
||||
@abstractmethod
|
||||
def _get_work_strategies(self) -> list[WorkSearchStrategy]:
|
||||
"""Define work search strategies in priority order."""
|
||||
...
|
||||
|
||||
async def _find_work_prioritized(self) -> UUID | None:
|
||||
"""
|
||||
Find work using prioritized strategies.
|
||||
|
||||
First checks pending work queue, then tries each strategy
|
||||
in priority order.
|
||||
"""
|
||||
# Check pending queue first
|
||||
if hasattr(self, "_pending_work") and self._pending_work:
|
||||
return self._pending_work.pop(0)
|
||||
|
||||
# Try strategies in priority order
|
||||
strategies = sorted(self._get_work_strategies(), key=lambda s: s.priority)
|
||||
for strategy in strategies:
|
||||
task_id = await strategy.finder()
|
||||
if task_id:
|
||||
return task_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PROGRESS TRACKER
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProgressUpdate:
|
||||
"""A progress update for a task."""
|
||||
|
||||
message: str
|
||||
percentage: int
|
||||
details: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class ProgressTracker:
|
||||
"""
|
||||
Mixin for tracking and reporting progress.
|
||||
|
||||
Provides unified progress reporting that saves to task AND
|
||||
sends channel messages.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def _api_call(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
||||
"""Make API call (implemented in base Agent)."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def send_message(
|
||||
self,
|
||||
session_id: UUID | None,
|
||||
content: str,
|
||||
message_type: str,
|
||||
task_id: UUID | None = None,
|
||||
) -> None:
|
||||
"""Send message to session (implemented in base Agent)."""
|
||||
...
|
||||
|
||||
async def _report_progress(
|
||||
self,
|
||||
task_id: UUID,
|
||||
session_id: UUID | None,
|
||||
message: str,
|
||||
percentage: int,
|
||||
) -> None:
|
||||
"""
|
||||
Report progress for a task.
|
||||
|
||||
Saves to task record AND sends message to session.
|
||||
|
||||
Args:
|
||||
task_id: Task to update
|
||||
session_id: Session to notify (from context)
|
||||
message: Progress message
|
||||
percentage: Completion percentage (0-100)
|
||||
"""
|
||||
# Save to task (suppress errors - logged by _api_call)
|
||||
with contextlib.suppress(Exception):
|
||||
await self._api_call(
|
||||
"POST",
|
||||
f"/tasks/{task_id}/progress",
|
||||
json={"message": message, "percentage": percentage},
|
||||
)
|
||||
|
||||
# Send session message
|
||||
task_ref = str(task_id)[:8]
|
||||
await self.send_message(
|
||||
session_id,
|
||||
f"TASK-{task_ref} ({percentage}%) {message}",
|
||||
message_type="action",
|
||||
task_id=task_id,
|
||||
)
|
||||
@@ -1,388 +0,0 @@
|
||||
"""
|
||||
Agent Orchestrator
|
||||
|
||||
Manages the lifecycle of all agents in the system.
|
||||
Handles spawning, monitoring, and coordination.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.agents.base import Agent
|
||||
from roboco.models import AgentRole, AgentStatus, Team
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class Orchestrator:
|
||||
"""
|
||||
Central orchestrator for all RoboCo agents.
|
||||
|
||||
Responsibilities:
|
||||
- Spawn and stop agents
|
||||
- Monitor agent health
|
||||
- Route messages between agents
|
||||
- Handle agent failures and restarts
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the orchestrator."""
|
||||
self._agents: dict[UUID, Agent] = {}
|
||||
self._agent_tasks: dict[UUID, asyncio.Task] = {}
|
||||
self._running = False
|
||||
self._monitor_task: asyncio.Task | None = None
|
||||
|
||||
self.log = logger.bind(component="orchestrator")
|
||||
|
||||
@property
|
||||
def agents(self) -> dict[UUID, Agent]:
|
||||
"""Get all registered agents."""
|
||||
return self._agents.copy()
|
||||
|
||||
@property
|
||||
def active_agents(self) -> list[Agent]:
|
||||
"""Get all active agents."""
|
||||
return [
|
||||
a for a in self._agents.values() if a.state.status == AgentStatus.ACTIVE
|
||||
]
|
||||
|
||||
@property
|
||||
def idle_agents(self) -> list[Agent]:
|
||||
"""Get all idle agents."""
|
||||
return [a for a in self._agents.values() if a.state.status == AgentStatus.IDLE]
|
||||
|
||||
# =========================================================================
|
||||
# LIFECYCLE
|
||||
# =========================================================================
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the orchestrator."""
|
||||
if self._running:
|
||||
self.log.warning("Orchestrator already running")
|
||||
return
|
||||
|
||||
self.log.info("Starting orchestrator")
|
||||
self._running = True
|
||||
|
||||
# Start health monitor
|
||||
self._monitor_task = asyncio.create_task(self._health_monitor())
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the orchestrator and all agents."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self.log.info("Stopping orchestrator")
|
||||
self._running = False
|
||||
|
||||
# Stop health monitor
|
||||
if self._monitor_task:
|
||||
self._monitor_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self._monitor_task
|
||||
|
||||
# Stop all agents
|
||||
await self.stop_all_agents()
|
||||
|
||||
self.log.info("Orchestrator stopped")
|
||||
|
||||
# =========================================================================
|
||||
# AGENT MANAGEMENT
|
||||
# =========================================================================
|
||||
|
||||
def register_agent(self, agent: Agent) -> None:
|
||||
"""
|
||||
Register an agent with the orchestrator.
|
||||
|
||||
Args:
|
||||
agent: The agent to register
|
||||
"""
|
||||
if agent.id in self._agents:
|
||||
self.log.warning("Agent already registered", agent_id=str(agent.id))
|
||||
return
|
||||
|
||||
self._agents[agent.id] = agent
|
||||
self.log.info(
|
||||
"Agent registered",
|
||||
agent_id=str(agent.id),
|
||||
agent_name=agent.name,
|
||||
agent_role=agent.role.value,
|
||||
)
|
||||
|
||||
def unregister_agent(self, agent_id: UUID) -> None:
|
||||
"""
|
||||
Unregister an agent.
|
||||
|
||||
Args:
|
||||
agent_id: ID of the agent to unregister
|
||||
"""
|
||||
if agent_id not in self._agents:
|
||||
return
|
||||
|
||||
agent = self._agents.pop(agent_id)
|
||||
self.log.info(
|
||||
"Agent unregistered", agent_id=str(agent_id), agent_name=agent.name
|
||||
)
|
||||
|
||||
async def spawn_agent(self, agent: Agent) -> None:
|
||||
"""
|
||||
Spawn an agent (register and start).
|
||||
|
||||
Args:
|
||||
agent: The agent to spawn
|
||||
"""
|
||||
self.register_agent(agent)
|
||||
await agent.start()
|
||||
self.log.info("Agent spawned", agent_id=str(agent.id), agent_name=agent.name)
|
||||
|
||||
async def stop_agent(self, agent_id: UUID) -> None:
|
||||
"""
|
||||
Stop a specific agent.
|
||||
|
||||
Args:
|
||||
agent_id: ID of the agent to stop
|
||||
"""
|
||||
if agent_id not in self._agents:
|
||||
self.log.warning("Agent not found", agent_id=str(agent_id))
|
||||
return
|
||||
|
||||
agent = self._agents[agent_id]
|
||||
await agent.stop()
|
||||
self.log.info("Agent stopped", agent_id=str(agent_id), agent_name=agent.name)
|
||||
|
||||
async def stop_all_agents(self) -> None:
|
||||
"""Stop all registered agents."""
|
||||
self.log.info("Stopping all agents", count=len(self._agents))
|
||||
|
||||
# Stop all agents concurrently
|
||||
await asyncio.gather(
|
||||
*[agent.stop() for agent in self._agents.values()],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
async def restart_agent(self, agent_id: UUID) -> None:
|
||||
"""
|
||||
Restart an agent.
|
||||
|
||||
Args:
|
||||
agent_id: ID of the agent to restart
|
||||
"""
|
||||
if agent_id not in self._agents:
|
||||
self.log.warning("Agent not found", agent_id=str(agent_id))
|
||||
return
|
||||
|
||||
agent = self._agents[agent_id]
|
||||
self.log.info("Restarting agent", agent_id=str(agent_id), agent_name=agent.name)
|
||||
|
||||
await agent.stop()
|
||||
await asyncio.sleep(1) # Brief pause
|
||||
await agent.start()
|
||||
|
||||
# =========================================================================
|
||||
# QUERYING
|
||||
# =========================================================================
|
||||
|
||||
def get_agent(self, agent_id: UUID) -> Agent | None:
|
||||
"""Get an agent by ID."""
|
||||
return self._agents.get(agent_id)
|
||||
|
||||
def get_agent_by_slug(self, slug: str) -> Agent | None:
|
||||
"""Get an agent by slug."""
|
||||
for agent in self._agents.values():
|
||||
if agent.config.slug == slug:
|
||||
return agent
|
||||
return None
|
||||
|
||||
def get_agents_by_role(self, role: AgentRole) -> list[Agent]:
|
||||
"""Get all agents with a specific role."""
|
||||
return [a for a in self._agents.values() if a.role == role]
|
||||
|
||||
def get_agents_by_team(self, team: Team) -> list[Agent]:
|
||||
"""Get all agents in a specific team."""
|
||||
return [a for a in self._agents.values() if a.team == team]
|
||||
|
||||
def get_cell_agents(self, team: Team) -> dict[str, list[Agent]]:
|
||||
"""
|
||||
Get agents organized by role for a team/cell.
|
||||
|
||||
Returns:
|
||||
Dict with keys: developers, qa, pm, documenter
|
||||
"""
|
||||
team_agents = self.get_agents_by_team(team)
|
||||
return {
|
||||
"developers": [a for a in team_agents if a.role == AgentRole.DEVELOPER],
|
||||
"qa": [a for a in team_agents if a.role == AgentRole.QA],
|
||||
"pm": [a for a in team_agents if a.role == AgentRole.CELL_PM],
|
||||
"documenter": [a for a in team_agents if a.role == AgentRole.DOCUMENTER],
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# HEALTH MONITORING
|
||||
# =========================================================================
|
||||
|
||||
async def _health_monitor(self) -> None:
|
||||
"""
|
||||
Monitor agent health periodically.
|
||||
|
||||
Checks for:
|
||||
- Unresponsive agents
|
||||
- Agents with errors
|
||||
- Agents that need restart
|
||||
"""
|
||||
while self._running:
|
||||
try:
|
||||
await self._check_agent_health()
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
self.log.error("Error in health monitor", error=str(e))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _check_agent_health(self) -> None:
|
||||
"""Check health of all agents."""
|
||||
now = datetime.now(UTC)
|
||||
|
||||
for agent in self._agents.values():
|
||||
# Check for errors
|
||||
if agent.state.error:
|
||||
self.log.warning(
|
||||
"Agent has error",
|
||||
agent_id=str(agent.id),
|
||||
error=agent.state.error,
|
||||
)
|
||||
|
||||
# Check for inactivity (5 minutes)
|
||||
minutes_in_seconds = 300
|
||||
if agent.state.last_activity:
|
||||
inactive_seconds = (now - agent.state.last_activity).total_seconds()
|
||||
if inactive_seconds > minutes_in_seconds and agent.is_running:
|
||||
self.log.warning(
|
||||
"Agent inactive",
|
||||
agent_id=str(agent.id),
|
||||
inactive_seconds=inactive_seconds,
|
||||
)
|
||||
|
||||
def get_health_status(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get overall health status of all agents.
|
||||
|
||||
Returns:
|
||||
Health status summary
|
||||
"""
|
||||
total = len(self._agents)
|
||||
by_status: dict[str, int] = {}
|
||||
errors: list[dict[str, str]] = []
|
||||
|
||||
for agent in self._agents.values():
|
||||
status = agent.state.status.value
|
||||
by_status[status] = by_status.get(status, 0) + 1
|
||||
|
||||
if agent.state.error:
|
||||
errors.append(
|
||||
{
|
||||
"agent_id": str(agent.id),
|
||||
"agent_name": agent.name,
|
||||
"error": agent.state.error,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"total_agents": total,
|
||||
"by_status": by_status,
|
||||
"errors": errors,
|
||||
"healthy": len(errors) == 0,
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# CELL MANAGEMENT
|
||||
# =========================================================================
|
||||
|
||||
async def spawn_cell(
|
||||
self,
|
||||
team: Team,
|
||||
agent_factory: Callable[[Team], list[Agent]],
|
||||
) -> list[Agent]:
|
||||
"""
|
||||
Spawn all agents for a cell.
|
||||
|
||||
Args:
|
||||
team: The team/cell to spawn
|
||||
agent_factory: Factory function that creates agents for the team
|
||||
|
||||
Returns:
|
||||
List of spawned agents
|
||||
"""
|
||||
self.log.info("Spawning cell", team=team.value)
|
||||
|
||||
agents = agent_factory(team)
|
||||
for agent in agents:
|
||||
await self.spawn_agent(agent)
|
||||
|
||||
return agents
|
||||
|
||||
async def stop_cell(self, team: Team) -> None:
|
||||
"""
|
||||
Stop all agents in a cell.
|
||||
|
||||
Args:
|
||||
team: The team/cell to stop
|
||||
"""
|
||||
self.log.info("Stopping cell", team=team.value)
|
||||
|
||||
team_agents = self.get_agents_by_team(team)
|
||||
await asyncio.gather(
|
||||
*[self.stop_agent(a.id) for a in team_agents],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# SERIALIZATION
|
||||
# =========================================================================
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Get orchestrator status as dictionary."""
|
||||
return {
|
||||
"running": self._running,
|
||||
"total_agents": len(self._agents),
|
||||
"agents": [a.to_dict() for a in self._agents.values()],
|
||||
"health": self.get_health_status(),
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SINGLETON HOLDER
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class _OrchestratorHolder:
|
||||
"""Holder class for singleton orchestrator instance."""
|
||||
|
||||
instance: Orchestrator | None = None
|
||||
|
||||
|
||||
def get_orchestrator() -> Orchestrator:
|
||||
"""Get or create the global orchestrator instance."""
|
||||
if _OrchestratorHolder.instance is None:
|
||||
_OrchestratorHolder.instance = Orchestrator()
|
||||
return _OrchestratorHolder.instance
|
||||
|
||||
|
||||
async def start_orchestrator() -> Orchestrator:
|
||||
"""Start the global orchestrator."""
|
||||
orchestrator = get_orchestrator()
|
||||
await orchestrator.start()
|
||||
return orchestrator
|
||||
|
||||
|
||||
async def stop_orchestrator() -> None:
|
||||
"""Stop the global orchestrator."""
|
||||
if _OrchestratorHolder.instance:
|
||||
await _OrchestratorHolder.instance.stop()
|
||||
_OrchestratorHolder.instance = None
|
||||
-1521
File diff suppressed because it is too large
Load Diff
@@ -1,493 +0,0 @@
|
||||
"""
|
||||
QA Agent
|
||||
|
||||
Implementation of the QA workflow from the blueprint.
|
||||
Handles review lifecycle:
|
||||
MONITOR → RECEIVE → UNDERSTAND → TEST → VERDICT → DOCUMENT → RETURN
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.agents.base import Agent, AgentConfig
|
||||
from roboco.agents.mixins import PhaseConfig, PhaseEngine
|
||||
from roboco.models.agents import (
|
||||
QATaskPhase,
|
||||
ReviewContext,
|
||||
TestCase,
|
||||
TestResult,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class QAAgent(Agent, PhaseEngine[QATaskPhase, ReviewContext]):
|
||||
"""
|
||||
QA agent that follows the QA Lifecycle.
|
||||
|
||||
Workflow:
|
||||
1. MONITOR - Watch cell channel, track tasks approaching completion
|
||||
2. RECEIVE - Dev flags ready, claim review task
|
||||
3. UNDERSTAND - Read requirements, dev notes, commits
|
||||
4. TEST - Execute test scenarios, edge cases
|
||||
5. VERDICT - PASS or FAIL with clear feedback
|
||||
6. DOCUMENT - Add QA notes, test coverage
|
||||
7. RETURN - Back to monitoring
|
||||
"""
|
||||
|
||||
def __init__(self, config: AgentConfig) -> None:
|
||||
"""Initialize QA agent."""
|
||||
super().__init__(config)
|
||||
self._review_context: ReviewContext | None = None
|
||||
self._cell_channel_id: UUID | None = None
|
||||
self._pending_reviews: list[UUID] = []
|
||||
|
||||
async def _initialize(self) -> None:
|
||||
"""Initialize QA-specific resources."""
|
||||
self.log.debug("QA agent initialized", agent_id=str(self.id))
|
||||
|
||||
async def _cleanup(self) -> None:
|
||||
"""Cleanup QA-specific resources."""
|
||||
self._review_context = None
|
||||
self._pending_reviews.clear()
|
||||
self.log.debug("QA agent cleanup complete", agent_id=str(self.id))
|
||||
|
||||
# =========================================================================
|
||||
# PHASE ENGINE IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
def _get_phase_configs(self) -> list[PhaseConfig[QATaskPhase]]:
|
||||
"""Define the QA workflow phases."""
|
||||
return [
|
||||
PhaseConfig(
|
||||
QATaskPhase.RECEIVE,
|
||||
self._phase_receive,
|
||||
next_phase=QATaskPhase.UNDERSTAND,
|
||||
),
|
||||
PhaseConfig(
|
||||
QATaskPhase.UNDERSTAND,
|
||||
self._phase_understand,
|
||||
next_phase=QATaskPhase.TEST,
|
||||
),
|
||||
PhaseConfig(
|
||||
QATaskPhase.TEST,
|
||||
self._phase_test,
|
||||
next_phase=QATaskPhase.VERDICT,
|
||||
requires_completion=True,
|
||||
),
|
||||
PhaseConfig(
|
||||
QATaskPhase.VERDICT,
|
||||
self._phase_verdict,
|
||||
next_phase=QATaskPhase.DOCUMENT,
|
||||
),
|
||||
PhaseConfig(
|
||||
QATaskPhase.DOCUMENT,
|
||||
self._phase_document,
|
||||
next_phase=QATaskPhase.RETURN,
|
||||
),
|
||||
PhaseConfig(
|
||||
QATaskPhase.RETURN,
|
||||
self._phase_return,
|
||||
next_phase=None, # Terminal
|
||||
),
|
||||
]
|
||||
|
||||
def _get_current_phase(self, ctx: ReviewContext) -> QATaskPhase:
|
||||
"""Get the current phase from context."""
|
||||
return ctx.phase
|
||||
|
||||
def _set_current_phase(self, ctx: ReviewContext, phase: QATaskPhase) -> None:
|
||||
"""Set the current phase in context."""
|
||||
ctx.phase = phase
|
||||
|
||||
# =========================================================================
|
||||
# LIFECYCLE IMPLEMENTATION
|
||||
# =========================================================================
|
||||
|
||||
async def find_work(self) -> UUID | None:
|
||||
"""
|
||||
MONITOR phase: Watch for tasks ready for review.
|
||||
|
||||
- Check for tasks flagged as awaiting_qa
|
||||
- Check for PM notifications
|
||||
"""
|
||||
self.log.info("Monitoring for reviews")
|
||||
|
||||
# Check pending reviews queue
|
||||
if self._pending_reviews:
|
||||
return self._pending_reviews.pop(0)
|
||||
|
||||
# Query for tasks awaiting QA
|
||||
task_id = await self._find_awaiting_qa()
|
||||
if task_id:
|
||||
return task_id
|
||||
|
||||
return None
|
||||
|
||||
async def execute_task(self, task_id: UUID) -> bool:
|
||||
"""
|
||||
Execute review through QA lifecycle phases.
|
||||
|
||||
Returns True when review is complete.
|
||||
"""
|
||||
if self._review_context is None or self._review_context.task_id != task_id:
|
||||
title, session_id = await self._get_task_info(task_id)
|
||||
self._review_context = ReviewContext(
|
||||
task_id=task_id,
|
||||
title=title,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
ctx = self._review_context
|
||||
|
||||
try:
|
||||
result = await self._run_phase_engine(ctx)
|
||||
|
||||
if result.error:
|
||||
self.log.error(
|
||||
"Error in review phase",
|
||||
phase=ctx.phase.value,
|
||||
error=result.error,
|
||||
)
|
||||
ctx.findings.append(f"Error during review: {result.error}")
|
||||
return False
|
||||
|
||||
if result.completed:
|
||||
self._review_context = None
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.log.error(
|
||||
"Error in review phase",
|
||||
phase=ctx.phase.value,
|
||||
error=str(e),
|
||||
)
|
||||
ctx.findings.append(f"Error during review: {e}")
|
||||
return False
|
||||
|
||||
# =========================================================================
|
||||
# PHASE IMPLEMENTATIONS
|
||||
# =========================================================================
|
||||
|
||||
async def _phase_receive(self, ctx: ReviewContext) -> None:
|
||||
"""
|
||||
RECEIVE phase: Claim the review task.
|
||||
|
||||
- Claim task via /claim endpoint
|
||||
- Acknowledge receipt
|
||||
- Announce review started
|
||||
"""
|
||||
self.log.info("RECEIVE phase", task_id=str(ctx.task_id))
|
||||
|
||||
# CLAIM: Transition from awaiting_qa to claimed
|
||||
await self._mark_claimed(ctx.task_id)
|
||||
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"Starting review of TASK-{str(ctx.task_id)[:8]}: {ctx.title}",
|
||||
message_type="action",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
|
||||
ctx.notes.append(f"[{datetime.now(UTC).isoformat()}] Review started")
|
||||
|
||||
async def _phase_understand(self, ctx: ReviewContext) -> None:
|
||||
"""
|
||||
UNDERSTAND phase: Read requirements and dev notes.
|
||||
|
||||
- Read task requirements and acceptance criteria
|
||||
- Read dev's handoff notes (from task's dev_notes field)
|
||||
- Review commits
|
||||
- Check conversation history
|
||||
- Read developer's journal entries for this task (if needed)
|
||||
"""
|
||||
self.log.info("UNDERSTAND phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Read task context
|
||||
requirements = await self._read_task_requirements(ctx.task_id)
|
||||
dev_notes = await self._read_dev_notes(ctx.task_id)
|
||||
commits = await self._get_task_commits_formatted(ctx.task_id)
|
||||
|
||||
# Read developer journal entries for this task (cell members can read)
|
||||
dev_journal = await self._read_team_journal_for_task(ctx.task_id)
|
||||
|
||||
# Use TOON for token-efficient context encoding
|
||||
task_context = self._format_review_context(
|
||||
ctx.title, requirements, dev_notes, commits
|
||||
)
|
||||
|
||||
# Include journal context if available
|
||||
journal_context = ""
|
||||
if dev_journal:
|
||||
journal_context = f"\n\nDeveloper Journal Entries:\n{dev_journal}"
|
||||
|
||||
prompt = f"""You are a QA engineer reviewing a completed task.
|
||||
|
||||
{task_context}{journal_context}
|
||||
|
||||
Based on this, create test cases to verify the implementation.
|
||||
|
||||
Focus on:
|
||||
- Acceptance criteria verification
|
||||
- Edge cases
|
||||
- Integration points
|
||||
- Error handling
|
||||
|
||||
If acceptance criteria mentions journaling requirements, verify them against
|
||||
the developer journal entries provided above.
|
||||
|
||||
Format response as TOON tabular:
|
||||
[N,]{{name,description,steps,expected}}:
|
||||
Acceptance Criteria,Verify all criteria met,Review implementation|Check each criterion,All criteria satisfied
|
||||
""" # noqa: E501
|
||||
_response = await self.think(prompt)
|
||||
|
||||
# Create test cases (simplified parsing)
|
||||
ctx.test_cases = [
|
||||
TestCase(
|
||||
name="Acceptance Criteria",
|
||||
description="Verify all acceptance criteria are met",
|
||||
steps=["Review implementation", "Check each criterion"],
|
||||
expected="All criteria satisfied",
|
||||
),
|
||||
TestCase(
|
||||
name="Edge Cases",
|
||||
description="Test edge cases and error handling",
|
||||
steps=["Test with invalid input", "Test boundary conditions"],
|
||||
expected="Graceful handling of edge cases",
|
||||
),
|
||||
TestCase(
|
||||
name="Integration",
|
||||
description="Verify integration with existing code",
|
||||
steps=["Run integration tests", "Check API compatibility"],
|
||||
expected="No breaking changes",
|
||||
),
|
||||
]
|
||||
|
||||
# PLAN: Save test plan to task API (required before start)
|
||||
plan_data = {
|
||||
"approach": f"QA review of {ctx.title}",
|
||||
"sub_tasks": [
|
||||
{
|
||||
"id": f"test-{i}",
|
||||
"title": tc.name,
|
||||
"description": tc.description,
|
||||
"completed": False,
|
||||
"order": i,
|
||||
}
|
||||
for i, tc in enumerate(ctx.test_cases)
|
||||
],
|
||||
"risks": [],
|
||||
}
|
||||
await self._api_call("PATCH", f"/tasks/{ctx.task_id}", json={"plan": plan_data})
|
||||
|
||||
ts = datetime.now(UTC).isoformat()
|
||||
ctx.notes.append(f"[{ts}] Created {len(ctx.test_cases)} test cases")
|
||||
|
||||
async def _phase_test(self, ctx: ReviewContext) -> bool:
|
||||
"""
|
||||
TEST phase: Execute test scenarios.
|
||||
|
||||
- START: Transition to in_progress on first test
|
||||
- Run through each test case
|
||||
- Document findings
|
||||
|
||||
Returns True when all tests complete.
|
||||
"""
|
||||
self.log.info(
|
||||
"TEST phase",
|
||||
task_id=str(ctx.task_id),
|
||||
test=ctx.current_test,
|
||||
total=len(ctx.test_cases),
|
||||
)
|
||||
|
||||
# START: Transition to in_progress on first test
|
||||
if ctx.current_test == 0:
|
||||
await self._mark_in_progress(ctx.task_id)
|
||||
self.log.info("QA review started (in_progress)", task_id=str(ctx.task_id))
|
||||
|
||||
if ctx.current_test >= len(ctx.test_cases):
|
||||
return True
|
||||
|
||||
test_case = ctx.test_cases[ctx.current_test]
|
||||
|
||||
# Use TOON for token-efficient context encoding
|
||||
test_context = self._format_test_context(
|
||||
test_case.name,
|
||||
test_case.description,
|
||||
test_case.steps,
|
||||
test_case.expected,
|
||||
)
|
||||
|
||||
prompt = f"""Execute this test case:
|
||||
|
||||
{test_context}
|
||||
|
||||
Simulate executing this test and provide results.
|
||||
|
||||
Format response as TOON:
|
||||
{{result,actual,notes}}:
|
||||
PASS,All criteria verified successfully,No issues found
|
||||
"""
|
||||
response = await self.think(prompt)
|
||||
|
||||
# Parse result (simplified)
|
||||
if "PASS" in response.upper():
|
||||
test_case.result = TestResult.PASS
|
||||
else:
|
||||
test_case.result = TestResult.FAIL
|
||||
ctx.findings.append(f"FAIL: {test_case.name}")
|
||||
|
||||
test_case.actual = response
|
||||
ctx.current_test += 1
|
||||
|
||||
# Progress update
|
||||
progress = f"{ctx.current_test}/{len(ctx.test_cases)}"
|
||||
result_str = test_case.result.value.upper()
|
||||
task_ref = str(ctx.task_id)[:8]
|
||||
msg = f"TASK-{task_ref} test {progress}: {test_case.name} - {result_str}"
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
msg,
|
||||
message_type="action",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
|
||||
return ctx.current_test >= len(ctx.test_cases)
|
||||
|
||||
async def _phase_verdict(self, ctx: ReviewContext) -> None:
|
||||
"""
|
||||
VERDICT phase: Determine overall pass/fail.
|
||||
|
||||
- Analyze all test results
|
||||
- Communicate clear verdict
|
||||
- If fail, provide specific feedback
|
||||
"""
|
||||
self.log.info("VERDICT phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Determine verdict
|
||||
failed_tests = [t for t in ctx.test_cases if t.result == TestResult.FAIL]
|
||||
|
||||
if failed_tests:
|
||||
ctx.verdict = TestResult.FAIL
|
||||
|
||||
# Communicate failure with specifics
|
||||
failure_summary = "\n".join(
|
||||
[f"- {t.name}: {t.actual or 'No details'}" for t in failed_tests]
|
||||
)
|
||||
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"TASK-{str(ctx.task_id)[:8]} QA FAILED\n\n"
|
||||
f"Issues found:\n{failure_summary}\n\n"
|
||||
f"Task returned to developer for fixes.",
|
||||
message_type="decision",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
|
||||
# Use proper QA fail endpoint (handles notes, status, reassignment)
|
||||
issue_list = [t.actual or t.name for t in failed_tests]
|
||||
await self._qa_fail(
|
||||
ctx.task_id,
|
||||
"Found issues that need fixing before approval.",
|
||||
issue_list,
|
||||
)
|
||||
|
||||
else:
|
||||
ctx.verdict = TestResult.PASS
|
||||
|
||||
await self.send_message(
|
||||
ctx.session_id,
|
||||
f"TASK-{str(ctx.task_id)[:8]} QA APPROVED\n\n"
|
||||
f"All {len(ctx.test_cases)} tests passed.\n"
|
||||
f"Ready for documentation.",
|
||||
message_type="decision",
|
||||
task_id=ctx.task_id,
|
||||
)
|
||||
|
||||
# Use proper QA pass endpoint (handles notes, status)
|
||||
await self._qa_pass(
|
||||
ctx.task_id,
|
||||
f"All {len(ctx.test_cases)} tests passed. Ready for documentation.",
|
||||
)
|
||||
|
||||
ctx.notes.append(
|
||||
f"[{datetime.now(UTC).isoformat()}] Verdict: {ctx.verdict.value.upper()}"
|
||||
)
|
||||
|
||||
async def _phase_document(self, ctx: ReviewContext) -> None:
|
||||
"""
|
||||
DOCUMENT phase: Add QA notes to task.
|
||||
|
||||
- Document test coverage
|
||||
- Add handoff notes for documenter
|
||||
"""
|
||||
self.log.info("DOCUMENT phase", task_id=str(ctx.task_id))
|
||||
|
||||
# Generate QA report
|
||||
test_summary = "\n".join(
|
||||
[
|
||||
f"- {t.name}: {t.result.value.upper() if t.result else 'NOT RUN'}"
|
||||
for t in ctx.test_cases
|
||||
]
|
||||
)
|
||||
|
||||
qa_report = f"""
|
||||
## QA Review Summary
|
||||
|
||||
**Task**: {ctx.title}
|
||||
**Verdict**: {ctx.verdict.value.upper() if ctx.verdict else "UNKNOWN"}
|
||||
**Reviewed**: {datetime.now(UTC).isoformat()}
|
||||
|
||||
### Tests Executed
|
||||
|
||||
{test_summary}
|
||||
|
||||
### Findings
|
||||
|
||||
{chr(10).join(ctx.findings) if ctx.findings else "No issues found"}
|
||||
|
||||
### Notes
|
||||
|
||||
{chr(10).join(ctx.notes)}
|
||||
"""
|
||||
|
||||
# Would save to task record
|
||||
self.log.info("QA report generated", report_length=len(qa_report))
|
||||
|
||||
ctx.notes.append(f"[{datetime.now(UTC).isoformat()}] QA documentation complete")
|
||||
|
||||
async def _phase_return(self, ctx: ReviewContext) -> None:
|
||||
"""
|
||||
RETURN phase: Clean up and return to monitoring.
|
||||
"""
|
||||
self.log.info("RETURN phase", task_id=str(ctx.task_id))
|
||||
# Context will be cleared by execute_task on completion
|
||||
|
||||
# =========================================================================
|
||||
# HELPER METHODS
|
||||
# =========================================================================
|
||||
|
||||
async def _find_awaiting_qa(self) -> UUID | None:
|
||||
"""Find tasks awaiting QA review."""
|
||||
try:
|
||||
team_param = self.team.value if self.team else None
|
||||
result = await self._api_call(
|
||||
"GET",
|
||||
"/tasks",
|
||||
params={"status": "awaiting_qa", "team": team_param},
|
||||
)
|
||||
tasks = result.get("items", [])
|
||||
return UUID(tasks[0]["id"]) if tasks else None
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to find awaiting QA task", error=str(e))
|
||||
return None
|
||||
|
||||
async def _get_task_commits_formatted(self, task_id: UUID) -> str:
|
||||
"""Get commits for the task as formatted string."""
|
||||
commits = await self._get_task_commits(task_id)
|
||||
return "\n".join(commits) if commits else "No commits recorded"
|
||||
+28
-20
@@ -84,11 +84,11 @@ AGENT_TEAM_MAP: Final[dict[str, str]] = {
|
||||
"fe-qa": "frontend",
|
||||
"fe-pm": "frontend",
|
||||
"fe-doc": "frontend",
|
||||
# UX/UI cell
|
||||
"ux-dev": "uxui",
|
||||
"ux-qa": "uxui",
|
||||
"ux-pm": "uxui",
|
||||
"ux-doc": "uxui",
|
||||
# UX/UI cell (matches Team.UX_UI = "ux_ui")
|
||||
"ux-dev": "ux_ui",
|
||||
"ux-qa": "ux_ui",
|
||||
"ux-pm": "ux_ui",
|
||||
"ux-doc": "ux_ui",
|
||||
# Management has no team
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ AGENT_TEAM_MAP: Final[dict[str, str]] = {
|
||||
CELL_MEMBERS: Final[dict[str, list[str]]] = {
|
||||
"backend": ["be-dev-1", "be-dev-2", "be-qa", "be-pm", "be-doc"],
|
||||
"frontend": ["fe-dev-1", "fe-dev-2", "fe-qa", "fe-pm", "fe-doc"],
|
||||
"uxui": ["ux-dev", "ux-qa", "ux-pm", "ux-doc"],
|
||||
"ux_ui": ["ux-dev", "ux-qa", "ux-pm", "ux-doc"],
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,12 @@ BOARD_MEMBERS: Final[list[str]] = ["product-owner", "head-marketing", "auditor"]
|
||||
# All PMs
|
||||
ALL_PMS: Final[list[str]] = ["be-pm", "fe-pm", "ux-pm", "main-pm"]
|
||||
|
||||
# All by role (cross-cell)
|
||||
ALL_DEVS: Final[list[str]] = ["be-dev-1", "be-dev-2", "fe-dev-1", "fe-dev-2", "ux-dev"]
|
||||
ALL_QA: Final[list[str]] = ["be-qa", "fe-qa", "ux-qa"]
|
||||
ALL_DOCS: Final[list[str]] = ["be-doc", "fe-doc", "ux-doc"]
|
||||
CELL_PMS: Final[list[str]] = ["be-pm", "fe-pm", "ux-pm"]
|
||||
|
||||
# PM-capable roles (can create and assign tasks)
|
||||
PM_ROLES: Final[set[str]] = {
|
||||
"cell_pm",
|
||||
@@ -248,41 +254,43 @@ def get_escalation_target(agent_id: str) -> str | None:
|
||||
# =============================================================================
|
||||
|
||||
CHANNEL_ACCESS: Final[dict[str, dict[str, list[str]]]] = {
|
||||
# Cell channels - members read/write, auditor silent
|
||||
# Cell channels - members read/write, main-pm read (monitoring), auditor silent
|
||||
"backend-cell": {
|
||||
"read": CELL_MEMBERS["backend"],
|
||||
"read": [*CELL_MEMBERS["backend"], "main-pm"],
|
||||
"write": CELL_MEMBERS["backend"],
|
||||
"silent": ["auditor"],
|
||||
},
|
||||
"frontend-cell": {
|
||||
"read": CELL_MEMBERS["frontend"],
|
||||
"read": [*CELL_MEMBERS["frontend"], "main-pm"],
|
||||
"write": CELL_MEMBERS["frontend"],
|
||||
"silent": ["auditor"],
|
||||
},
|
||||
"uxui-cell": {
|
||||
"read": CELL_MEMBERS["uxui"],
|
||||
"write": CELL_MEMBERS["uxui"],
|
||||
"read": [*CELL_MEMBERS["ux_ui"], "main-pm"],
|
||||
"write": CELL_MEMBERS["ux_ui"],
|
||||
"silent": ["auditor"],
|
||||
},
|
||||
# Cross-cell role channels
|
||||
# Cell members read/write their role channel
|
||||
# Cell PMs read/write ALL cross-cell channels for coordination
|
||||
"dev-all": {
|
||||
"read": ["be-dev-1", "be-dev-2", "fe-dev-1", "fe-dev-2", "ux-dev"],
|
||||
"write": ["be-dev-1", "be-dev-2", "fe-dev-1", "fe-dev-2", "ux-dev"],
|
||||
"read": [*ALL_DEVS, *ALL_QA, *ALL_DOCS, *CELL_PMS, "main-pm"],
|
||||
"write": [*ALL_DEVS, *CELL_PMS, "main-pm"],
|
||||
"silent": ["auditor"],
|
||||
},
|
||||
"qa-all": {
|
||||
"read": ["be-qa", "fe-qa", "ux-qa"],
|
||||
"write": ["be-qa", "fe-qa", "ux-qa"],
|
||||
"read": [*ALL_QA, *ALL_DEVS, *ALL_DOCS, *CELL_PMS, "main-pm"],
|
||||
"write": [*ALL_QA, *CELL_PMS],
|
||||
"silent": ["auditor"],
|
||||
},
|
||||
"pm-all": {
|
||||
"read": ["be-pm", "fe-pm", "ux-pm", "main-pm"],
|
||||
"write": ["be-pm", "fe-pm", "ux-pm", "main-pm"],
|
||||
"read": [*CELL_PMS, "main-pm"],
|
||||
"write": [*CELL_PMS, "main-pm"],
|
||||
"silent": ["auditor"],
|
||||
},
|
||||
"doc-all": {
|
||||
"read": ["be-doc", "fe-doc", "ux-doc"],
|
||||
"write": ["be-doc", "fe-doc", "ux-doc"],
|
||||
"read": [*ALL_DOCS, *CELL_PMS, "main-pm"],
|
||||
"write": [*ALL_DOCS, *CELL_PMS],
|
||||
"silent": ["auditor"],
|
||||
},
|
||||
# Management channels
|
||||
@@ -292,7 +300,7 @@ CHANNEL_ACCESS: Final[dict[str, dict[str, list[str]]]] = {
|
||||
"silent": [],
|
||||
},
|
||||
"board-private": {
|
||||
"read": ["product-owner", "head-marketing", "auditor", "ceo"],
|
||||
"read": ["product-owner", "head-marketing", "auditor", "ceo", "main-pm"],
|
||||
"write": ["product-owner", "head-marketing", "auditor", "ceo"],
|
||||
"silent": [],
|
||||
},
|
||||
|
||||
+22
-22
@@ -40,7 +40,7 @@ from roboco.api.schemas.tasks import (
|
||||
transform_update_data,
|
||||
)
|
||||
from roboco.db.tables import AgentTable, NotificationTable
|
||||
from roboco.models.base import TaskStatus, Team
|
||||
from roboco.models.base import AgentRole, TaskStatus, Team
|
||||
from roboco.models.task import TaskCreate
|
||||
from roboco.services.audit import get_audit_service
|
||||
from roboco.services.messaging import get_messaging_service
|
||||
@@ -518,7 +518,8 @@ async def start_task(
|
||||
detail="Only the assigned agent can start this task",
|
||||
)
|
||||
|
||||
task = await service.start(task_id)
|
||||
# Pass agent_id for defense-in-depth validation in service layer
|
||||
task = await service.start(task_id, agent_id=agent.agent_id)
|
||||
if not task:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -544,9 +545,9 @@ async def block_task(
|
||||
)
|
||||
|
||||
# Only assigned agent or PM can block a task
|
||||
if task.assigned_to != agent.agent_id and agent.role.value not in (
|
||||
"cell_pm",
|
||||
"main_pm",
|
||||
if task.assigned_to != agent.agent_id and agent.role not in (
|
||||
AgentRole.CELL_PM,
|
||||
AgentRole.MAIN_PM,
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
@@ -587,9 +588,9 @@ async def soft_block_task(
|
||||
)
|
||||
|
||||
# Only assigned agent or PM can block a task
|
||||
if task.assigned_to != agent.agent_id and agent.role.value not in (
|
||||
"cell_pm",
|
||||
"main_pm",
|
||||
if task.assigned_to != agent.agent_id and agent.role not in (
|
||||
AgentRole.CELL_PM,
|
||||
AgentRole.MAIN_PM,
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
@@ -623,9 +624,9 @@ async def unblock_task(
|
||||
)
|
||||
|
||||
# Only assigned agent or PM can unblock a task
|
||||
if task.assigned_to != agent.agent_id and agent.role.value not in (
|
||||
"cell_pm",
|
||||
"main_pm",
|
||||
if task.assigned_to != agent.agent_id and agent.role not in (
|
||||
AgentRole.CELL_PM,
|
||||
AgentRole.MAIN_PM,
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
@@ -782,7 +783,7 @@ async def pass_qa(
|
||||
)
|
||||
|
||||
# Only QA agents can pass/fail QA
|
||||
if agent.role.value != "qa":
|
||||
if agent.role != AgentRole.QA:
|
||||
audit = get_audit_service()
|
||||
await audit.log_task_action_denial(
|
||||
agent_id=agent.agent_id,
|
||||
@@ -819,7 +820,7 @@ async def pass_qa(
|
||||
if not task:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot pass QA - not awaiting QA",
|
||||
detail="Cannot pass QA - invalid status for QA workflow",
|
||||
)
|
||||
await db.commit()
|
||||
return task_to_response(task)
|
||||
@@ -841,7 +842,7 @@ async def fail_qa(
|
||||
)
|
||||
|
||||
# Only QA agents can pass/fail QA
|
||||
if agent.role.value != "qa":
|
||||
if agent.role != AgentRole.QA:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only QA agents can fail QA reviews",
|
||||
@@ -861,7 +862,7 @@ async def fail_qa(
|
||||
if not task:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot fail QA - not awaiting QA",
|
||||
detail="Cannot fail QA - invalid status for QA workflow",
|
||||
)
|
||||
await db.commit()
|
||||
return task_to_response(task)
|
||||
@@ -887,7 +888,7 @@ async def docs_complete(
|
||||
)
|
||||
|
||||
# Only documenter role can mark docs complete
|
||||
if agent.role.value != "documenter":
|
||||
if agent.role != AgentRole.DOCUMENTER:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only documenters can mark documentation as complete",
|
||||
@@ -914,7 +915,7 @@ async def docs_complete(
|
||||
if not task:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot mark docs complete - task not awaiting documentation",
|
||||
detail="Cannot mark docs complete - invalid status for documenter workflow",
|
||||
)
|
||||
await db.commit()
|
||||
return task_to_response(task)
|
||||
@@ -1119,11 +1120,10 @@ async def escalate_task(
|
||||
detail=f"Escalation target not found: {target_slug}",
|
||||
)
|
||||
|
||||
# Create escalation notification directly (bypassing permission checks)
|
||||
body = (
|
||||
f"Task {task_id} escalated by {agent_record.slug}.\n\n"
|
||||
f"Reason: {data.reason}"
|
||||
)
|
||||
# Create escalation notification directly
|
||||
# NOTE: Permission is enforced via get_escalation_target() which constrains
|
||||
# the escalation chain (devs→PM, PM→MainPM, etc.) per agents_config
|
||||
body = f"Task {task_id} escalated by {agent_record.slug}.\n\nReason: {data.reason}"
|
||||
notification = NotificationTable(
|
||||
type="blocker_escalation",
|
||||
priority="high",
|
||||
|
||||
+46
-3
@@ -5,6 +5,11 @@ Real-time communication via WebSocket connections for:
|
||||
- Channel streams (all messages in a channel)
|
||||
- Agent streams (individual agent output)
|
||||
- Session streams (messages in a session)
|
||||
|
||||
Security Note:
|
||||
WebSocket connections validate agent_id via query params and verify
|
||||
the agent exists in the database. In production, this should be
|
||||
enhanced with proper token-based authentication (JWT, etc.).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -20,6 +25,8 @@ from roboco.api.schemas.websocket import (
|
||||
NewMessageBroadcast,
|
||||
)
|
||||
from roboco.config import settings
|
||||
from roboco.db.base import get_db
|
||||
from roboco.services.repositories import resolve_agent_uuid
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -177,6 +184,24 @@ class ConnectionManager:
|
||||
manager = ConnectionManager()
|
||||
|
||||
|
||||
async def validate_agent_exists(agent_id: UUID | str) -> bool:
|
||||
"""
|
||||
Validate that an agent exists in the database.
|
||||
|
||||
This provides basic security by ensuring the claimed agent_id
|
||||
is a valid agent, not just a valid UUID format.
|
||||
|
||||
TODO: Enhance with token-based authentication (JWT) for production.
|
||||
"""
|
||||
try:
|
||||
async for db in get_db():
|
||||
result = await resolve_agent_uuid(db, str(agent_id))
|
||||
return result is not None
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
async def validate_channel_access(channel_id: UUID, agent_id: UUID) -> bool:
|
||||
"""
|
||||
Validate that an agent has access to a channel.
|
||||
@@ -286,6 +311,11 @@ async def agent_stream(
|
||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||
return
|
||||
|
||||
# Validate viewer agent exists in database
|
||||
if not await validate_agent_exists(viewer_id):
|
||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||
return
|
||||
|
||||
await manager.connect_agent(websocket, agent_id, viewer_id)
|
||||
|
||||
try:
|
||||
@@ -327,6 +357,11 @@ async def session_stream(
|
||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||
return
|
||||
|
||||
# Validate agent exists in database
|
||||
if not await validate_agent_exists(agent_id):
|
||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||
return
|
||||
|
||||
await manager.connect_session(websocket, session_id, agent_id)
|
||||
|
||||
try:
|
||||
@@ -356,6 +391,11 @@ async def notification_stream(
|
||||
|
||||
Agents receive real-time notifications via this stream.
|
||||
"""
|
||||
# Validate agent exists in database
|
||||
if not await validate_agent_exists(agent_id):
|
||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||
return
|
||||
|
||||
await manager.connect_notifications(websocket, agent_id)
|
||||
|
||||
try:
|
||||
@@ -397,16 +437,19 @@ async def broadcast_new_message(msg: NewMessageBroadcast) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def broadcast_agent_chunk(agent_id: UUID, chunk: str) -> None:
|
||||
async def broadcast_agent_chunk(
|
||||
agent_id: str, chunk: str, metadata: dict[str, Any]
|
||||
) -> None:
|
||||
"""Broadcast an agent stream chunk to watchers."""
|
||||
event = {
|
||||
"type": "agent.stream",
|
||||
"agent_id": str(agent_id),
|
||||
"agent_id": agent_id,
|
||||
"chunk": chunk,
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
**metadata,
|
||||
}
|
||||
|
||||
await manager.broadcast_to_agent_watchers(agent_id, event)
|
||||
await manager.broadcast_to_agent_watchers(UUID(agent_id), event)
|
||||
|
||||
|
||||
async def broadcast_session_closed(
|
||||
|
||||
+1
-2
@@ -11,13 +11,12 @@ from pathlib import Path
|
||||
import structlog
|
||||
import uvicorn
|
||||
|
||||
from roboco.agents import set_reasoning_stream_callback
|
||||
from roboco.api.deps import set_orchestrator
|
||||
from roboco.api.websocket import broadcast_agent_chunk
|
||||
from roboco.config import settings
|
||||
from roboco.db import bootstrap_database
|
||||
from roboco.events import EventBus, register_default_handlers, set_event_context
|
||||
from roboco.runtime import AgentOrchestrator
|
||||
from roboco.runtime import AgentOrchestrator, set_reasoning_stream_callback
|
||||
from roboco.services.notification import NotificationService
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
+52
-24
@@ -93,10 +93,10 @@ class AgentTable(Base):
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
|
||||
DateTime(timezone=True), onupdate=lambda: datetime.now(UTC), nullable=True
|
||||
)
|
||||
|
||||
# Relationships - use lazy="joined" for single optional relationship
|
||||
@@ -156,10 +156,10 @@ class TaskTable(Base):
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
|
||||
DateTime(timezone=True), onupdate=lambda: datetime.now(UTC), nullable=True
|
||||
)
|
||||
claimed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
@@ -279,10 +279,10 @@ class ChannelTable(Base):
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
|
||||
DateTime(timezone=True), onupdate=lambda: datetime.now(UTC), nullable=True
|
||||
)
|
||||
|
||||
# Relationships - use lazy="select" for collections to avoid N+1
|
||||
@@ -340,10 +340,10 @@ class GroupTable(Base):
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
|
||||
DateTime(timezone=True), onupdate=lambda: datetime.now(UTC), nullable=True
|
||||
)
|
||||
|
||||
# Relationships - use lazy="select" for collections to avoid N+1
|
||||
@@ -402,10 +402,10 @@ class SessionTable(Base):
|
||||
|
||||
# Timestamps
|
||||
started_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
last_activity_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
closed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
@@ -417,7 +417,7 @@ class SessionTable(Base):
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
|
||||
# Relationships - CRITICAL: use lazy="select" for messages (sessions can have 100+)
|
||||
@@ -480,7 +480,7 @@ class SessionTaskTable(Base):
|
||||
|
||||
# Audit
|
||||
added_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
added_by: Mapped[UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
@@ -585,7 +585,10 @@ class MessageTable(Base):
|
||||
|
||||
# Metadata
|
||||
timestamp: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False, index=True
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(UTC),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
|
||||
# Extraction metadata
|
||||
@@ -600,7 +603,7 @@ class MessageTable(Base):
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
|
||||
# Relationships - use lazy="joined" for agent to avoid N+1 on message lists
|
||||
@@ -670,7 +673,10 @@ class NotificationTable(Base):
|
||||
|
||||
# Timing
|
||||
timestamp: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False, index=True
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(UTC),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
expires_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
@@ -688,7 +694,7 @@ class NotificationTable(Base):
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
|
||||
# Relationships
|
||||
@@ -741,10 +747,10 @@ class JournalTable(Base):
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
|
||||
DateTime(timezone=True), onupdate=lambda: datetime.now(UTC), nullable=True
|
||||
)
|
||||
|
||||
# Relationships - use lazy="select" for entries collection to avoid N+1
|
||||
@@ -789,7 +795,10 @@ class JournalEntryTable(Base):
|
||||
|
||||
# Metadata
|
||||
timestamp: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False, index=True
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(UTC),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
tags: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
|
||||
|
||||
@@ -801,10 +810,10 @@ class JournalEntryTable(Base):
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
|
||||
DateTime(timezone=True), onupdate=lambda: datetime.now(UTC), nullable=True
|
||||
)
|
||||
|
||||
# Relationships
|
||||
@@ -826,7 +835,26 @@ class JournalEntryTable(Base):
|
||||
|
||||
|
||||
class HandoffTable(Base):
|
||||
"""SQLAlchemy table for documentation handoffs."""
|
||||
"""
|
||||
SQLAlchemy table for structured documentation handoffs.
|
||||
|
||||
STATUS: RESERVED FOR FUTURE USE
|
||||
===============================
|
||||
This table exists in the schema but has no service layer or API yet.
|
||||
|
||||
Current Implementation:
|
||||
Handoffs use the simpler `dev_notes` + `handoff_summary` parameters
|
||||
in `roboco_task_submit_qa()`, stored directly on the task.
|
||||
|
||||
Future Enhancement:
|
||||
This table enables richer, structured handoff documents with:
|
||||
- Categorized changes (new functionality, breaking changes)
|
||||
- Required vs optional documentation items
|
||||
- Code samples, gotchas, key learnings
|
||||
- Linked commits and file locations
|
||||
|
||||
To implement: create HandoffService + API routes + MCP tools.
|
||||
"""
|
||||
|
||||
__tablename__ = "handoffs"
|
||||
|
||||
@@ -895,10 +923,10 @@ class HandoffTable(Base):
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=datetime.now(UTC), nullable=False
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), onupdate=datetime.now(UTC), nullable=True
|
||||
DateTime(timezone=True), onupdate=lambda: datetime.now(UTC), nullable=True
|
||||
)
|
||||
claimed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
|
||||
@@ -9,6 +9,15 @@ Provides rule enforcement for all RoboCo operations:
|
||||
- Message validation
|
||||
- Session boundaries
|
||||
- Handoff requirements
|
||||
|
||||
Available Utilities (may not all be in use yet):
|
||||
- Transition helpers: get_valid_transitions, can_agent_transition
|
||||
- State checks: is_terminal_state, is_active_state, is_waiting_state
|
||||
- Channel utilities: get_agent_channels
|
||||
- QA utilities: can_review_task
|
||||
|
||||
All functions are designed to be imported as needed. Some are internal
|
||||
helpers used by the primary validate_* functions.
|
||||
"""
|
||||
|
||||
from roboco.enforcement.channel_access import (
|
||||
|
||||
@@ -15,12 +15,14 @@ __all__ = ["VALID_TRANSITIONS", "TaskLifecycleError", "validate_task_transition"
|
||||
# =============================================================================
|
||||
|
||||
VALID_TRANSITIONS: dict[str, list[str]] = {
|
||||
# Initial state
|
||||
# PM setup phase - task with dependencies or needs session setup
|
||||
"backlog": ["pending", "cancelled"],
|
||||
# Ready for work state
|
||||
"pending": ["claimed", "cancelled"],
|
||||
# Claimed - can start, unclaim, or cancel
|
||||
"claimed": ["in_progress", "pending", "cancelled"],
|
||||
# In progress - can block, pause, submit for verification, or cancel
|
||||
"in_progress": ["blocked", "paused", "verifying", "cancelled"],
|
||||
# In progress - can block, pause, verify, complete (PM only), or cancel
|
||||
"in_progress": ["blocked", "paused", "verifying", "completed", "cancelled"],
|
||||
# Blocked - can unblock back to in_progress or cancel
|
||||
"blocked": ["in_progress", "cancelled"],
|
||||
# Paused - can resume back to in_progress or cancel
|
||||
@@ -34,10 +36,16 @@ VALID_TRANSITIONS: dict[str, list[str]] = {
|
||||
],
|
||||
# Needs revision - back to work or cancel
|
||||
"needs_revision": ["in_progress", "cancelled"],
|
||||
# Awaiting QA - can pass (to docs), fail (needs revision), block, or cancel
|
||||
"awaiting_qa": ["awaiting_documentation", "needs_revision", "blocked", "cancelled"],
|
||||
# Awaiting documentation - documenter marks docs done, goes to PM review
|
||||
"awaiting_documentation": ["awaiting_pm_review", "cancelled"],
|
||||
# Awaiting QA - QA claims, passes, fails, or blocks
|
||||
"awaiting_qa": [
|
||||
"claimed",
|
||||
"awaiting_documentation",
|
||||
"needs_revision",
|
||||
"blocked",
|
||||
"cancelled",
|
||||
],
|
||||
# Awaiting documentation - documenter claims or marks done
|
||||
"awaiting_documentation": ["claimed", "awaiting_pm_review", "cancelled"],
|
||||
# Awaiting PM review - PM reviews and completes, or cancels
|
||||
"awaiting_pm_review": ["completed", "cancelled"],
|
||||
# Terminal states - cannot transition out
|
||||
@@ -56,14 +64,20 @@ _CANCEL_ROLES = ["cell_pm", "main_pm", "product_owner", "head_marketing"]
|
||||
|
||||
# Transitions that require specific roles
|
||||
ROLE_RESTRICTED_TRANSITIONS: dict[tuple[str, str], list[str]] = {
|
||||
# Only QA can pass or fail QA
|
||||
# Only PM can activate tasks from backlog
|
||||
("backlog", "pending"): _CANCEL_ROLES,
|
||||
# Only QA can claim and perform QA actions
|
||||
("awaiting_qa", "claimed"): ["qa"],
|
||||
("awaiting_qa", "awaiting_documentation"): ["qa"],
|
||||
("awaiting_qa", "needs_revision"): ["qa"],
|
||||
# Only documenter can mark docs complete
|
||||
# Only documenter can claim docs tasks and mark complete
|
||||
("awaiting_documentation", "claimed"): ["documenter"],
|
||||
("awaiting_documentation", "awaiting_pm_review"): ["documenter"],
|
||||
# Only PM can complete after PM review
|
||||
("awaiting_pm_review", "completed"): _CANCEL_ROLES, # PMs complete tasks
|
||||
# Only PM can complete tasks (either after PM review or their own work)
|
||||
("awaiting_pm_review", "completed"): _CANCEL_ROLES,
|
||||
("in_progress", "completed"): _CANCEL_ROLES, # PM completing their own task
|
||||
# Only PM or higher can cancel tasks (all states that allow cancel)
|
||||
("backlog", "cancelled"): _CANCEL_ROLES,
|
||||
("pending", "cancelled"): _CANCEL_ROLES,
|
||||
("claimed", "cancelled"): _CANCEL_ROLES,
|
||||
("in_progress", "cancelled"): _CANCEL_ROLES,
|
||||
|
||||
+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)
|
||||
|
||||
@@ -144,7 +144,12 @@ class JournalEntryType(str, Enum):
|
||||
|
||||
|
||||
class HandoffStatus(str, Enum):
|
||||
"""Documenter handoff states."""
|
||||
"""
|
||||
Documenter handoff states.
|
||||
|
||||
NOTE: Reserved for future HandoffTable implementation.
|
||||
Currently unused - see HandoffTable docstring for details.
|
||||
"""
|
||||
|
||||
PENDING = "pending"
|
||||
CLAIMED = "claimed"
|
||||
|
||||
@@ -3,6 +3,13 @@ Handoff Model
|
||||
|
||||
Documenter handoffs contain all the information needed for
|
||||
a Documenter to create production documentation from developer work.
|
||||
|
||||
STATUS: RESERVED FOR FUTURE USE
|
||||
================================
|
||||
These models define structured handoff documents but are not yet
|
||||
implemented in the service layer. See HandoffTable docstring for details.
|
||||
|
||||
Current workflow uses simpler `dev_notes + handoff_summary` on tasks.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
"""
|
||||
Organization Models
|
||||
|
||||
Domain types for the organizational structure (cells, board, organization).
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
import structlog
|
||||
|
||||
from roboco.models import Team
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from roboco.agents.base import Agent
|
||||
from roboco.agents.board import (
|
||||
AuditorAgent,
|
||||
HeadMarketingAgent,
|
||||
ProductOwnerAgent,
|
||||
)
|
||||
from roboco.agents.developer import DeveloperAgent
|
||||
from roboco.agents.documenter import DocumenterAgent
|
||||
from roboco.agents.pm import CellPMAgent, MainPMAgent
|
||||
from roboco.agents.qa import QAAgent
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Cell:
|
||||
"""A complete cell with all its agents."""
|
||||
|
||||
name: str
|
||||
team: Team
|
||||
pm: "CellPMAgent"
|
||||
developers: list["DeveloperAgent"]
|
||||
qa: "QAAgent"
|
||||
documenter: "DocumenterAgent"
|
||||
|
||||
@property
|
||||
def all_agents(self) -> list["Agent"]:
|
||||
"""Get all agents in the cell."""
|
||||
return [self.pm, *self.developers, self.qa, self.documenter]
|
||||
|
||||
async def start_all(self) -> None:
|
||||
"""Start all agents in the cell."""
|
||||
for agent in self.all_agents:
|
||||
await agent.start()
|
||||
logger.info("Cell started", cell=self.name, agents=len(self.all_agents))
|
||||
|
||||
async def stop_all(self) -> None:
|
||||
"""Stop all agents in the cell."""
|
||||
for agent in self.all_agents:
|
||||
await agent.stop()
|
||||
logger.info("Cell stopped", cell=self.name)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Board:
|
||||
"""The board level with all board agents."""
|
||||
|
||||
product_owner: "ProductOwnerAgent"
|
||||
head_marketing: "HeadMarketingAgent"
|
||||
auditor: "AuditorAgent"
|
||||
|
||||
@property
|
||||
def all_agents(self) -> list["Agent"]:
|
||||
"""Get all board agents."""
|
||||
return [self.product_owner, self.head_marketing, self.auditor]
|
||||
|
||||
async def start_all(self) -> None:
|
||||
"""Start all board agents."""
|
||||
for agent in self.all_agents:
|
||||
await agent.start()
|
||||
logger.info("Board started", agents=len(self.all_agents))
|
||||
|
||||
async def stop_all(self) -> None:
|
||||
"""Stop all board agents."""
|
||||
for agent in self.all_agents:
|
||||
await agent.stop()
|
||||
logger.info("Board stopped")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Organization:
|
||||
"""The complete AI organization."""
|
||||
|
||||
board: Board
|
||||
main_pm: "MainPMAgent"
|
||||
backend_cell: Cell
|
||||
frontend_cell: Cell
|
||||
ux_cell: Cell
|
||||
|
||||
@property
|
||||
def all_agents(self) -> list["Agent"]:
|
||||
"""Get all agents in the organization."""
|
||||
agents: list[Agent] = []
|
||||
agents.extend(self.board.all_agents)
|
||||
agents.append(self.main_pm)
|
||||
agents.extend(self.backend_cell.all_agents)
|
||||
agents.extend(self.frontend_cell.all_agents)
|
||||
agents.extend(self.ux_cell.all_agents)
|
||||
return agents
|
||||
|
||||
@property
|
||||
def agent_count(self) -> int:
|
||||
"""Total number of agents."""
|
||||
return len(self.all_agents)
|
||||
|
||||
async def start_all(self) -> None:
|
||||
"""Start the entire organization."""
|
||||
logger.info("Starting organization")
|
||||
|
||||
# Start board first
|
||||
await self.board.start_all()
|
||||
await self.main_pm.start()
|
||||
|
||||
# Then cells
|
||||
await self.backend_cell.start_all()
|
||||
await self.frontend_cell.start_all()
|
||||
await self.ux_cell.start_all()
|
||||
|
||||
logger.info("Organization started", total_agents=self.agent_count)
|
||||
|
||||
async def stop_all(self) -> None:
|
||||
"""Stop the entire organization."""
|
||||
logger.info("Stopping organization")
|
||||
|
||||
# Stop cells first
|
||||
await self.ux_cell.stop_all()
|
||||
await self.frontend_cell.stop_all()
|
||||
await self.backend_cell.stop_all()
|
||||
|
||||
# Then management
|
||||
await self.main_pm.stop()
|
||||
await self.board.stop_all()
|
||||
|
||||
logger.info("Organization stopped")
|
||||
|
||||
def get_agent_by_id(self, agent_id: UUID) -> "Agent | None":
|
||||
"""Find an agent by ID."""
|
||||
for agent in self.all_agents:
|
||||
if agent.id == agent_id:
|
||||
return agent
|
||||
return None
|
||||
|
||||
def get_agent_by_slug(self, slug: str) -> "Agent | None":
|
||||
"""Find an agent by slug."""
|
||||
for agent in self.all_agents:
|
||||
if agent.config.slug == slug:
|
||||
return agent
|
||||
return None
|
||||
|
||||
def get_agents_by_team(self, team: Team) -> list["Agent"]:
|
||||
"""Get all agents in a team."""
|
||||
return [a for a in self.all_agents if a.team == team]
|
||||
@@ -5,9 +5,19 @@ Manages Claude Code agent instances, lifecycle, and orchestration.
|
||||
"""
|
||||
|
||||
from roboco.runtime.orchestrator import AgentInstance, AgentOrchestrator, AgentState
|
||||
from roboco.runtime.streaming import (
|
||||
ReasoningStreamCallback,
|
||||
get_reasoning_stream_callback,
|
||||
set_reasoning_stream_callback,
|
||||
stream_reasoning,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AgentInstance",
|
||||
"AgentOrchestrator",
|
||||
"AgentState",
|
||||
"ReasoningStreamCallback",
|
||||
"get_reasoning_stream_callback",
|
||||
"set_reasoning_stream_callback",
|
||||
"stream_reasoning",
|
||||
]
|
||||
|
||||
+216
-13
@@ -667,7 +667,10 @@ class AgentOrchestrator:
|
||||
}
|
||||
return role_map.get(agent_id, agent_id)
|
||||
|
||||
# Static team mappings for management agents
|
||||
# Static team mappings for management agents (ROUTING purposes)
|
||||
# NOTE: This differs from agents_config.get_agent_team() intentionally.
|
||||
# agents_config returns None for management (no team for permissions).
|
||||
# This map returns routing categories for dispatcher task assignment.
|
||||
_AGENT_TEAM_MAP: ClassVar[dict[str, str]] = {
|
||||
"main-pm": "main_pm",
|
||||
"product-owner": "board",
|
||||
@@ -1155,6 +1158,23 @@ Start by:
|
||||
}
|
||||
)
|
||||
|
||||
# Keywords that indicate cross-cell work (requires Main PM)
|
||||
_CROSS_CELL_KEYWORDS = frozenset(
|
||||
{
|
||||
"all teams",
|
||||
"all cells",
|
||||
"every team",
|
||||
"every cell",
|
||||
"all departments",
|
||||
"cross-cell",
|
||||
"company-wide",
|
||||
"organization-wide",
|
||||
"backend and frontend",
|
||||
"frontend and backend",
|
||||
"all three",
|
||||
}
|
||||
)
|
||||
|
||||
def _has_board_keywords(self, text: str) -> bool:
|
||||
"""Check if text contains board-level keywords."""
|
||||
return any(kw in text for kw in self._BOARD_KEYWORDS)
|
||||
@@ -1163,6 +1183,10 @@ Start by:
|
||||
"""Check if text contains PM coordination keywords."""
|
||||
return any(kw in text for kw in self._PM_KEYWORDS)
|
||||
|
||||
def _has_cross_cell_keywords(self, text: str) -> bool:
|
||||
"""Check if text indicates work spanning multiple cells."""
|
||||
return any(kw in text for kw in self._CROSS_CELL_KEYWORDS)
|
||||
|
||||
# Direct team-to-routing mappings (explicit assignments bypass keyword analysis)
|
||||
_TEAM_ROUTING_MAP: ClassVar[dict[str, str]] = {
|
||||
"main_pm": "main_pm",
|
||||
@@ -1192,6 +1216,10 @@ Start by:
|
||||
if self._has_board_keywords(text):
|
||||
return "board"
|
||||
|
||||
# Cross-cell keywords (e.g., "all teams") → Main PM (regardless of complexity)
|
||||
if self._has_cross_cell_keywords(text):
|
||||
return "main_pm"
|
||||
|
||||
# High complexity or cross-team → Main PM
|
||||
if complexity in ("high", "critical") or not team or team == "all":
|
||||
return "main_pm"
|
||||
@@ -1242,8 +1270,87 @@ Start by:
|
||||
|
||||
return None
|
||||
|
||||
def _build_main_pm_triage_prompt(self, task: dict[str, Any]) -> str:
|
||||
"""Build prompt for MAIN PM to triage and distribute to Cell PMs."""
|
||||
task_id = task.get("id", "unknown")
|
||||
title = task.get("title", "Untitled")
|
||||
complexity = task.get("complexity", "medium")
|
||||
description = task.get("description", "")
|
||||
|
||||
return f"""You are the MAIN PM at RoboCo. This task is assigned to YOU.
|
||||
|
||||
TASK: {task_id}
|
||||
TITLE: {title}
|
||||
COMPLEXITY: {complexity}
|
||||
DESCRIPTION: {description[:500]}
|
||||
|
||||
YOUR JOB: Either work on this yourself OR distribute to Cell PMs.
|
||||
You do NOT assign to developers directly - Cell PMs manage their teams.
|
||||
|
||||
== WHO YOU ASSIGN TO ==
|
||||
|
||||
- Backend work → be-pm (who manages be-dev-1, be-dev-2)
|
||||
- Frontend work → fe-pm (who manages fe-dev-1, fe-dev-2)
|
||||
- UX/UI work → ux-pm (who manages ux-dev)
|
||||
|
||||
🚨 NEVER assign to be-dev-1, fe-dev-1, ux-dev directly. ONLY to Cell PMs.
|
||||
|
||||
== WHEN TO WORK ON IT YOURSELF ==
|
||||
|
||||
Work on the task yourself if it's:
|
||||
- PM work (validation, coordination, planning, reviews)
|
||||
- Communication tasks (announcements, status updates)
|
||||
- Something you can do directly without code changes
|
||||
- Cross-cell coordination that doesn't need delegation
|
||||
|
||||
If it makes sense for YOU to do it - just do it!
|
||||
|
||||
== MAIN PM WORKFLOW ==
|
||||
|
||||
1. GET TASK DETAILS
|
||||
roboco_task_get("{task_id}")
|
||||
|
||||
2. DECIDE: Keep or delegate?
|
||||
- Validation/coordination → Keep for yourself
|
||||
- Development work → Delegate to Cell PM(s)
|
||||
|
||||
3A. IF KEEPING: Work on it directly
|
||||
- roboco_task_plan("{task_id}", ...)
|
||||
- roboco_task_start("{task_id}")
|
||||
- Do the work
|
||||
- roboco_task_submit_pm_review("{task_id}")
|
||||
|
||||
3B. IF DELEGATING: Create tasks for Cell PMs
|
||||
For each cell that needs work:
|
||||
|
||||
roboco_task_create(
|
||||
title="Cell-specific task title",
|
||||
description="What needs to be done",
|
||||
team="backend", # or "frontend" or "ux_ui"
|
||||
acceptance_criteria=["criterion 1", "criterion 2"],
|
||||
assigned_to="be-pm", # Cell PM, NOT developer!
|
||||
status="backlog"
|
||||
)
|
||||
|
||||
Then: roboco_task_activate(task_id) for each task
|
||||
|
||||
4. LOG YOUR DECISION
|
||||
roboco_journal_decision(data)
|
||||
|
||||
5. FINISH
|
||||
roboco_agent_idle()
|
||||
|
||||
== CRITICAL RULES ==
|
||||
- NEVER assign directly to developers (be-dev-1, fe-dev-1, etc.)
|
||||
- Cell PMs delegate to their developers - that's THEIR job, not yours
|
||||
- For cross-cell work: create a task for EACH relevant cell
|
||||
- Validation tasks stay with you
|
||||
|
||||
Start now: roboco_task_get("{task_id}")
|
||||
"""
|
||||
|
||||
def _build_pm_triage_prompt(self, task: dict[str, Any]) -> str:
|
||||
"""Build prompt for PM to triage and delegate a task."""
|
||||
"""Build prompt for CELL PM to triage and delegate a task."""
|
||||
task_id = task.get("id", "unknown")
|
||||
title = task.get("title", "Untitled")
|
||||
complexity = task.get("complexity", "medium")
|
||||
@@ -1427,14 +1534,13 @@ Start now: roboco_task_get("{task_id}")
|
||||
tasks = await self._fetch_tasks(client, "pending")
|
||||
|
||||
# PM-level agents that can have direct assignments
|
||||
# NOTE: Only actual PMs, not board members (product-owner, etc.)
|
||||
# Board members are handled by their dedicated dispatch methods
|
||||
pm_agents = {
|
||||
"main-pm",
|
||||
"be-pm",
|
||||
"fe-pm",
|
||||
"ux-pm",
|
||||
"product-owner",
|
||||
"head-marketing",
|
||||
"auditor",
|
||||
}
|
||||
|
||||
for task in tasks:
|
||||
@@ -1449,10 +1555,16 @@ Start now: roboco_task_get("{task_id}")
|
||||
task_id=task.get("id"),
|
||||
agent_id=agent_slug,
|
||||
)
|
||||
# Use Main PM prompt for main-pm, Cell PM prompt for others
|
||||
pm_prompt = (
|
||||
self._build_main_pm_triage_prompt(task)
|
||||
if agent_slug == "main-pm"
|
||||
else self._build_pm_triage_prompt(task)
|
||||
)
|
||||
await self.spawn_agent(
|
||||
agent_id=agent_slug,
|
||||
task_id=task["id"],
|
||||
initial_prompt=self._build_pm_triage_prompt(task),
|
||||
initial_prompt=pm_prompt,
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -1482,9 +1594,11 @@ Start now: roboco_task_get("{task_id}")
|
||||
|
||||
# Claim and spawn with appropriate prompt
|
||||
if await self._claim_task_for_agent(client, task["id"], agent_id):
|
||||
# Use PM triage prompt for PMs, dev prompt for devs
|
||||
# Use appropriate prompt based on agent type
|
||||
if routing == "dev":
|
||||
prompt = self._build_dev_prompt(task)
|
||||
elif routing == "main_pm" or agent_id == "main-pm":
|
||||
prompt = self._build_main_pm_triage_prompt(task)
|
||||
else:
|
||||
prompt = self._build_pm_triage_prompt(task)
|
||||
|
||||
@@ -1624,18 +1738,31 @@ ALL SUBTASKS COMPLETED:
|
||||
Begin with step 1: roboco_task_get("{task_id}")
|
||||
"""
|
||||
|
||||
def _get_prompt_for_agent(self, agent_slug: str, task: dict[str, Any]) -> str:
|
||||
"""Get the appropriate prompt based on agent role."""
|
||||
role = get_agent_role(agent_slug)
|
||||
if role == "developer":
|
||||
return self._build_dev_prompt(task)
|
||||
elif role == "documenter":
|
||||
return self._build_doc_prompt(task)
|
||||
elif role == "qa":
|
||||
return self._build_qa_prompt(task)
|
||||
else:
|
||||
# PM or other - use dev prompt as fallback
|
||||
return self._build_dev_prompt(task)
|
||||
|
||||
async def _dispatch_dev_work(self, client: httpx.AsyncClient) -> None:
|
||||
"""
|
||||
Dispatch development work to developers.
|
||||
Dispatch assigned pending work to the assigned agent.
|
||||
|
||||
NOTE: This now only handles PRE-ASSIGNED tasks (assigned by PM) and
|
||||
NOTE: This handles PRE-ASSIGNED tasks (assigned by PM) and
|
||||
needs_revision tasks. New unassigned pending tasks are handled by
|
||||
_dispatch_pm_work() which routes them through the PM hierarchy.
|
||||
|
||||
Monitors: assigned pending tasks, needs_revision tasks
|
||||
Spawns: be-dev-1, be-dev-2, fe-dev-1, fe-dev-2, ux-dev
|
||||
Spawns: Any assigned agent (dev, doc, qa) with appropriate prompt
|
||||
"""
|
||||
# Get tasks needing dev attention
|
||||
# Get tasks needing attention
|
||||
tasks = await self._fetch_tasks(client, ["pending", "needs_revision"])
|
||||
|
||||
for task in tasks:
|
||||
@@ -1658,12 +1785,12 @@ Begin with step 1: roboco_task_get("{task_id}")
|
||||
continue
|
||||
|
||||
# For pending tasks that ARE already assigned (by PM),
|
||||
# spawn the assigned dev if not active
|
||||
# spawn the assigned agent with the appropriate prompt
|
||||
if agent_slug and not self._is_agent_active(agent_slug):
|
||||
await self.spawn_agent(
|
||||
agent_id=agent_slug,
|
||||
task_id=task["id"],
|
||||
initial_prompt=self._build_dev_prompt(task),
|
||||
initial_prompt=self._get_prompt_for_agent(agent_slug, task),
|
||||
)
|
||||
|
||||
async def _dispatch_qa_work(self, client: httpx.AsyncClient) -> None:
|
||||
@@ -1680,6 +1807,23 @@ Begin with step 1: roboco_task_get("{task_id}")
|
||||
if team not in ["backend", "frontend", "ux_ui"]:
|
||||
continue
|
||||
|
||||
assigned_to = task.get("assigned_to")
|
||||
|
||||
# If already assigned, check if that agent is running
|
||||
if assigned_to:
|
||||
assigned_slug = self._resolve_agent_slug(assigned_to)
|
||||
if self._is_agent_active(assigned_slug):
|
||||
# Agent is running, they'll handle it
|
||||
continue
|
||||
# Agent not running - spawn them to continue
|
||||
await self.spawn_agent(
|
||||
agent_id=assigned_slug,
|
||||
task_id=task["id"],
|
||||
initial_prompt=self._build_qa_prompt(task),
|
||||
)
|
||||
continue
|
||||
|
||||
# Unassigned task - select QA agent for this team
|
||||
agent_id = self._select_agent_for_cell(team, "qa")
|
||||
if not agent_id:
|
||||
continue
|
||||
@@ -1688,6 +1832,15 @@ Begin with step 1: roboco_task_get("{task_id}")
|
||||
# QA already running, they'll pick up on scan
|
||||
continue
|
||||
|
||||
# Claim the task for QA agent BEFORE spawning
|
||||
if not await self._claim_task_for_agent(client, task["id"], agent_id):
|
||||
logger.warning(
|
||||
"Failed to claim awaiting_qa task for QA",
|
||||
task_id=task["id"],
|
||||
agent_id=agent_id,
|
||||
)
|
||||
continue
|
||||
|
||||
# Spawn QA agent with task assignment
|
||||
await self.spawn_agent(
|
||||
agent_id=agent_id,
|
||||
@@ -1711,6 +1864,22 @@ Begin with step 1: roboco_task_get("{task_id}")
|
||||
if team not in ["backend", "frontend", "ux_ui"]:
|
||||
continue
|
||||
|
||||
assigned_to = task.get("assigned_to")
|
||||
|
||||
# If already assigned, check if that agent is running
|
||||
if assigned_to:
|
||||
assigned_slug = self._resolve_agent_slug(assigned_to)
|
||||
if self._is_agent_active(assigned_slug):
|
||||
continue
|
||||
# Agent not running - spawn them to continue
|
||||
await self.spawn_agent(
|
||||
agent_id=assigned_slug,
|
||||
task_id=task["id"],
|
||||
initial_prompt=self._build_doc_prompt(task),
|
||||
)
|
||||
continue
|
||||
|
||||
# Unassigned task - select documenter for this team
|
||||
agent_id = self._select_agent_for_cell(team, "doc")
|
||||
if not agent_id:
|
||||
continue
|
||||
@@ -1718,6 +1887,15 @@ Begin with step 1: roboco_task_get("{task_id}")
|
||||
if self._is_agent_active(agent_id):
|
||||
continue
|
||||
|
||||
# Claim the task for documenter BEFORE spawning
|
||||
if not await self._claim_task_for_agent(client, task["id"], agent_id):
|
||||
logger.warning(
|
||||
"Failed to claim awaiting_documentation task for doc",
|
||||
task_id=task["id"],
|
||||
agent_id=agent_id,
|
||||
)
|
||||
continue
|
||||
|
||||
await self.spawn_agent(
|
||||
agent_id=agent_id,
|
||||
task_id=task["id"],
|
||||
@@ -1739,11 +1917,36 @@ Begin with step 1: roboco_task_get("{task_id}")
|
||||
if team not in ["backend", "frontend", "ux_ui"]:
|
||||
continue
|
||||
|
||||
assigned_to = task.get("assigned_to")
|
||||
|
||||
# If already assigned, check if that agent is running
|
||||
if assigned_to:
|
||||
assigned_slug = self._resolve_agent_slug(assigned_to)
|
||||
if self._is_agent_active(assigned_slug):
|
||||
continue
|
||||
# Agent not running - spawn them to continue
|
||||
await self.spawn_agent(
|
||||
agent_id=assigned_slug,
|
||||
task_id=task["id"],
|
||||
initial_prompt=self._build_pm_review_prompt(task),
|
||||
)
|
||||
continue
|
||||
|
||||
# Unassigned task - select PM for this team
|
||||
pm_id = self._TEAM_PM_MAP.get(team, "be-pm")
|
||||
|
||||
if self._is_agent_active(pm_id):
|
||||
continue
|
||||
|
||||
# Claim the task for PM BEFORE spawning
|
||||
if not await self._claim_task_for_agent(client, task["id"], pm_id):
|
||||
logger.warning(
|
||||
"Failed to claim awaiting_pm_review task for PM",
|
||||
task_id=task["id"],
|
||||
agent_id=pm_id,
|
||||
)
|
||||
continue
|
||||
|
||||
await self.spawn_agent(
|
||||
agent_id=pm_id,
|
||||
task_id=task["id"],
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Agent Reasoning Stream Callback
|
||||
|
||||
Allows external systems (like WebSocket handlers) to receive agent reasoning
|
||||
as it happens. Used for real-time UI updates.
|
||||
"""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
# Type for reasoning stream callback
|
||||
# Called with (agent_id: str, chunk: str, metadata: dict)
|
||||
ReasoningStreamCallback = Callable[[str, str, dict[str, Any]], Awaitable[None]]
|
||||
|
||||
|
||||
class _CallbackHolder:
|
||||
"""Holder for the global reasoning stream callback."""
|
||||
|
||||
callback: ReasoningStreamCallback | None = None
|
||||
|
||||
|
||||
def set_reasoning_stream_callback(callback: ReasoningStreamCallback | None) -> None:
|
||||
"""
|
||||
Set the global callback for agent reasoning streams.
|
||||
|
||||
This is called during bootstrap to wire up WebSocket broadcasting.
|
||||
|
||||
Args:
|
||||
callback: Async function that receives (agent_id, chunk, metadata)
|
||||
"""
|
||||
_CallbackHolder.callback = callback
|
||||
|
||||
|
||||
def get_reasoning_stream_callback() -> ReasoningStreamCallback | None:
|
||||
"""Get the current reasoning stream callback."""
|
||||
return _CallbackHolder.callback
|
||||
|
||||
|
||||
async def stream_reasoning(
|
||||
agent_id: str,
|
||||
chunk: str,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Stream a reasoning chunk to the registered callback.
|
||||
|
||||
Args:
|
||||
agent_id: The agent producing the reasoning
|
||||
chunk: The text chunk to stream
|
||||
metadata: Optional metadata about the chunk
|
||||
"""
|
||||
if _CallbackHolder.callback is not None:
|
||||
await _CallbackHolder.callback(agent_id, chunk, metadata or {})
|
||||
@@ -273,6 +273,20 @@ DEFAULT_AGENTS: list[dict[str, Any]] = [
|
||||
|
||||
# =============================================================================
|
||||
# CHANNEL MEMBERSHIP
|
||||
#
|
||||
# This populates the database channel.members/writers fields for initial setup.
|
||||
#
|
||||
# NOTE: This is SEPARATE from roboco/agents_config.py CHANNEL_ACCESS which is
|
||||
# the runtime permission source of truth. The relationship is:
|
||||
#
|
||||
# 1. CHANNEL_MEMBERSHIPS (here) -> populates database channel.members
|
||||
# 2. CHANNEL_ACCESS (agents_config) -> used by PermissionService for checks
|
||||
# 3. Privileged roles (CEO, Auditor, Main PM) bypass membership via
|
||||
# has_privileged_access() in services/permissions.py
|
||||
#
|
||||
# This means main-pm isn't listed in board-private here but CAN read it
|
||||
# via the privileged role bypass. The seed data is for UI/listing purposes,
|
||||
# while CHANNEL_ACCESS is the actual permission enforcement.
|
||||
# =============================================================================
|
||||
|
||||
CEO_AGENT_ID = AGENT_UUIDS["ceo"]
|
||||
@@ -405,7 +419,7 @@ Check `roboco_task_scan(team="frontend")` for pending frontend tasks.
|
||||
- Prototyping
|
||||
- Accessibility
|
||||
|
||||
Check `roboco_task_scan(team="uxui")` for pending UX/UI tasks.
|
||||
Check `roboco_task_scan(team="ux_ui")` for pending UX/UI tasks.
|
||||
""",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -243,9 +243,8 @@ class PermissionService(SingletonService):
|
||||
|
||||
# "cell" scope means can only notify own cell members
|
||||
if scope == "cell":
|
||||
# Cell PM can only notify their own cell unless coordinating with PMs
|
||||
if recipient.role == AgentRole.CELL_PM:
|
||||
# PMs can notify other PMs for coordination
|
||||
# Cell PM can notify other PMs (Cell PMs or Main PM) for coordination
|
||||
if recipient.role in (AgentRole.CELL_PM, AgentRole.MAIN_PM):
|
||||
return True
|
||||
# Otherwise must be same team
|
||||
return sender.team == recipient.team
|
||||
|
||||
+108
-30
@@ -16,6 +16,7 @@ from roboco.db.tables import AgentTable, SessionTaskTable, TaskTable
|
||||
from roboco.enforcement import (
|
||||
TaskOwnershipError,
|
||||
validate_task_ownership,
|
||||
validate_task_transition,
|
||||
)
|
||||
from roboco.models.base import TaskStatus, Team
|
||||
from roboco.models.task import TaskCreateRequest
|
||||
@@ -57,7 +58,13 @@ def _get_valid_claim_statuses(
|
||||
if role == "qa":
|
||||
return {TaskStatus.AWAITING_QA}
|
||||
elif role == "documenter":
|
||||
return {TaskStatus.AWAITING_DOCUMENTATION}
|
||||
# Documenters can claim:
|
||||
# - PENDING: when PM assigns a docs task directly
|
||||
# - AWAITING_DOCUMENTATION: normal workflow after QA passes
|
||||
statuses = {TaskStatus.PENDING, TaskStatus.AWAITING_DOCUMENTATION}
|
||||
if allow_reassign:
|
||||
statuses.add(TaskStatus.CLAIMED)
|
||||
return statuses
|
||||
else:
|
||||
# Developer, PM, and other roles
|
||||
statuses = {TaskStatus.PENDING}
|
||||
@@ -110,6 +117,48 @@ class TaskService(BaseService):
|
||||
|
||||
service_name: ClassVar[str] = "task"
|
||||
|
||||
# =========================================================================
|
||||
# STATUS TRANSITION HELPER
|
||||
# =========================================================================
|
||||
|
||||
def _validate_and_set_status(
|
||||
self,
|
||||
task: TaskTable,
|
||||
new_status: TaskStatus,
|
||||
agent_role: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Validate and set task status with lifecycle enforcement.
|
||||
|
||||
This is the single point of truth for status changes. All transitions
|
||||
are validated against VALID_TRANSITIONS and ROLE_RESTRICTED_TRANSITIONS.
|
||||
|
||||
Args:
|
||||
task: The task to update
|
||||
new_status: Target status
|
||||
agent_role: Optional role for role-restricted transitions
|
||||
|
||||
Raises:
|
||||
TaskLifecycleError: If transition is invalid or role not permitted
|
||||
"""
|
||||
current = (
|
||||
task.status.value if isinstance(task.status, TaskStatus) else task.status
|
||||
)
|
||||
target = new_status.value if isinstance(new_status, TaskStatus) else new_status
|
||||
|
||||
# Validate the transition (raises TaskLifecycleError if invalid)
|
||||
validate_task_transition(current, target, agent_role)
|
||||
|
||||
# Apply the status change
|
||||
task.status = new_status
|
||||
self.log.info(
|
||||
"Task status transition",
|
||||
task_id=str(task.id),
|
||||
from_status=current,
|
||||
to_status=target,
|
||||
agent_role=agent_role,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# CRUD OPERATIONS
|
||||
# =========================================================================
|
||||
@@ -381,7 +430,7 @@ class TaskService(BaseService):
|
||||
Role-based claiming:
|
||||
- Developers/PMs: can claim PENDING tasks
|
||||
- QA: can claim AWAITING_QA tasks
|
||||
- Documenters: can claim AWAITING_DOCUMENTATION tasks
|
||||
- Documenters: can claim PENDING (direct assignment) or AWAITING_DOCUMENTATION
|
||||
"""
|
||||
task = await self.get(task_id)
|
||||
if not task:
|
||||
@@ -411,18 +460,17 @@ class TaskService(BaseService):
|
||||
task.assigned_to = cast("Any", agent_id)
|
||||
task.claimed_at = datetime.now(UTC)
|
||||
|
||||
# Transition to CLAIMED from any claimable status
|
||||
# (PENDING, AWAITING_QA, AWAITING_DOCUMENTATION all → CLAIMED)
|
||||
# Transition to CLAIMED - validated with role for proper enforcement
|
||||
agent_role = agent.role.value if agent and agent.role else None
|
||||
claimable_statuses = {
|
||||
TaskStatus.PENDING,
|
||||
TaskStatus.AWAITING_QA,
|
||||
TaskStatus.AWAITING_DOCUMENTATION,
|
||||
}
|
||||
if task.status in claimable_statuses:
|
||||
task.status = TaskStatus.CLAIMED
|
||||
self._validate_and_set_status(task, TaskStatus.CLAIMED, agent_role)
|
||||
|
||||
await self.session.flush()
|
||||
self.log.info("Task claimed", task_id=str(task_id), agent_id=str(agent_id))
|
||||
return task
|
||||
|
||||
async def start(
|
||||
@@ -490,10 +538,8 @@ class TaskService(BaseService):
|
||||
# Only update started_at if this is the first time starting
|
||||
if task.started_at is None:
|
||||
task.started_at = datetime.now(UTC)
|
||||
task.status = TaskStatus.IN_PROGRESS
|
||||
self._validate_and_set_status(task, TaskStatus.IN_PROGRESS)
|
||||
await self.session.flush()
|
||||
|
||||
self.log.info("Task started", task_id=str(task_id))
|
||||
return task
|
||||
|
||||
async def block(self, task_id: UUID, blocker_task_id: UUID) -> TaskTable | None:
|
||||
@@ -505,7 +551,7 @@ class TaskService(BaseService):
|
||||
if blocker_task_id not in task.dependency_ids:
|
||||
new_deps = [*task.dependency_ids, blocker_task_id]
|
||||
task.dependency_ids = new_deps
|
||||
task.status = TaskStatus.BLOCKED
|
||||
self._validate_and_set_status(task, TaskStatus.BLOCKED)
|
||||
await self.session.flush()
|
||||
|
||||
# Update the blocker task to reference this as blocked
|
||||
@@ -645,13 +691,15 @@ class TaskService(BaseService):
|
||||
if task.status != TaskStatus.VERIFYING:
|
||||
return None
|
||||
|
||||
# Store original developer BEFORE QA claims - this is the authoritative record
|
||||
# for self-review prevention. Storing here ensures we capture the developer
|
||||
# even if the task is reassigned before QA claims it.
|
||||
# Store original developer BEFORE clearing assignment - authoritative
|
||||
# record for self-review prevention (QA can't review own work).
|
||||
original_dev = str(task.assigned_to) if task.assigned_to else None
|
||||
if original_dev:
|
||||
task.quick_context = f"original_developer:{original_dev}"
|
||||
|
||||
# Clear assignment so QA can claim the task
|
||||
# The original developer is preserved in quick_context
|
||||
task.assigned_to = None
|
||||
task.self_verified = True
|
||||
task.status = TaskStatus.AWAITING_QA
|
||||
await self.session.flush()
|
||||
@@ -666,16 +714,29 @@ class TaskService(BaseService):
|
||||
async def pass_qa(
|
||||
self, task_id: UUID, notes: str | None = None
|
||||
) -> TaskTable | None:
|
||||
"""Mark task as passed QA."""
|
||||
"""Mark task as passed QA.
|
||||
|
||||
QA workflow: awaiting_qa → claimed → in_progress → pass_qa
|
||||
→ awaiting_documentation. Accept claimed/in_progress status.
|
||||
"""
|
||||
task = await self.get(task_id)
|
||||
if not task:
|
||||
return None
|
||||
|
||||
if task.status != TaskStatus.AWAITING_QA:
|
||||
# Accept tasks QA is actively working on (claimed or in_progress)
|
||||
# as well as awaiting_qa (for direct pass without starting)
|
||||
valid_statuses = {
|
||||
TaskStatus.AWAITING_QA,
|
||||
TaskStatus.CLAIMED,
|
||||
TaskStatus.IN_PROGRESS,
|
||||
}
|
||||
if task.status not in valid_statuses:
|
||||
return None
|
||||
|
||||
if notes:
|
||||
task.qa_notes = notes
|
||||
# Clear assignment so documenter can claim the task
|
||||
task.assigned_to = None
|
||||
task.qa_verified = True
|
||||
task.status = TaskStatus.AWAITING_DOCUMENTATION
|
||||
await self.session.flush()
|
||||
@@ -690,12 +751,21 @@ class TaskService(BaseService):
|
||||
When QA fails a task, it goes back to the original developer for revision.
|
||||
The original developer is extracted from quick_context which stores
|
||||
"original_developer:{uuid}" when the task was submitted to QA.
|
||||
|
||||
QA workflow: awaiting_qa → claimed → in_progress → fail_qa → needs_revision
|
||||
So we need to accept tasks in claimed or in_progress status.
|
||||
"""
|
||||
task = await self.get(task_id)
|
||||
if not task:
|
||||
return None
|
||||
|
||||
if task.status != TaskStatus.AWAITING_QA:
|
||||
# Accept tasks QA is actively working on
|
||||
valid_statuses = {
|
||||
TaskStatus.AWAITING_QA,
|
||||
TaskStatus.CLAIMED,
|
||||
TaskStatus.IN_PROGRESS,
|
||||
}
|
||||
if task.status not in valid_statuses:
|
||||
return None
|
||||
|
||||
task.qa_notes = notes
|
||||
@@ -732,8 +802,8 @@ class TaskService(BaseService):
|
||||
"""
|
||||
Mark documentation as complete (documenter only).
|
||||
|
||||
Transitions task from AWAITING_DOCUMENTATION to AWAITING_PM_REVIEW.
|
||||
The Cell PM will then review and call complete() to finish the task.
|
||||
Documenter workflow: awaiting_documentation → claim → plan → start
|
||||
→ docs_complete. Accept claimed/in_progress (documenter working).
|
||||
|
||||
Args:
|
||||
task_id: The task to mark docs complete
|
||||
@@ -746,9 +816,16 @@ class TaskService(BaseService):
|
||||
if not task:
|
||||
return None
|
||||
|
||||
if task.status != TaskStatus.AWAITING_DOCUMENTATION:
|
||||
# Accept documenter workflow statuses: awaiting_documentation, claimed,
|
||||
# in_progress (documenter actively working on documentation)
|
||||
valid_statuses = {
|
||||
TaskStatus.AWAITING_DOCUMENTATION,
|
||||
TaskStatus.CLAIMED,
|
||||
TaskStatus.IN_PROGRESS,
|
||||
}
|
||||
if task.status not in valid_statuses:
|
||||
self.log.warning(
|
||||
"Cannot mark docs complete - not awaiting documentation",
|
||||
"Cannot mark docs complete - invalid status for documenter workflow",
|
||||
task_id=str(task_id),
|
||||
current_status=task.status.value,
|
||||
)
|
||||
@@ -778,8 +855,9 @@ class TaskService(BaseService):
|
||||
else doc_context
|
||||
)
|
||||
|
||||
# Note: We don't auto-assign to PM here - PM will pick it up via scan
|
||||
# The task remains assigned to documenter until PM claims it
|
||||
# Clear assignment so PM can claim the task for review
|
||||
# Documenter info is preserved in quick_context
|
||||
task.assigned_to = None
|
||||
await self.session.flush()
|
||||
|
||||
self.log.info(
|
||||
@@ -884,25 +962,25 @@ class TaskService(BaseService):
|
||||
return None
|
||||
|
||||
task.completed_at = datetime.now(UTC)
|
||||
task.status = TaskStatus.COMPLETED
|
||||
# Validate transition with PM role requirement
|
||||
self._validate_and_set_status(task, TaskStatus.COMPLETED, "cell_pm")
|
||||
await self.session.flush()
|
||||
|
||||
# Unblock any tasks waiting on this one
|
||||
await self._unblock_dependents(task_id)
|
||||
|
||||
self.log.info("Task completed", task_id=str(task_id))
|
||||
return task
|
||||
|
||||
async def cancel(self, task_id: UUID) -> TaskTable | None:
|
||||
"""Cancel a task."""
|
||||
async def cancel(
|
||||
self, task_id: UUID, agent_role: str = "cell_pm"
|
||||
) -> TaskTable | None:
|
||||
"""Cancel a task (PM only)."""
|
||||
task = await self.get(task_id)
|
||||
if not task:
|
||||
return None
|
||||
|
||||
task.status = TaskStatus.CANCELLED
|
||||
# Validate transition with PM role requirement
|
||||
self._validate_and_set_status(task, TaskStatus.CANCELLED, agent_role)
|
||||
await self.session.flush()
|
||||
|
||||
self.log.info("Task cancelled", task_id=str(task_id))
|
||||
return task
|
||||
|
||||
async def _unblock_dependents(self, completed_task_id: UUID) -> None:
|
||||
|
||||
Reference in New Issue
Block a user