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
|
/docs
|
||||||
/data
|
/data
|
||||||
/#recycle
|
/#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_get(task_id)` - Get full task details with acceptance criteria
|
||||||
- `roboco_task_claim(task_id)` - Claim a pending task
|
- `roboco_task_claim(task_id)` - Claim a pending task
|
||||||
- `roboco_task_start(task_id)` - Begin work (moves to in_progress)
|
- `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_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_block(task_id, reason, blocker_type, what_needed)` - Mark blocked
|
||||||
- `roboco_task_unblock(task_id)` - Resume from blocked state
|
- `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
|
- Do NOT proceed until you understand the acceptance criteria
|
||||||
|
|
||||||
### 4. PLAN
|
### 4. PLAN
|
||||||
**Tool:** `roboco_task_plan(task_id, plan)`
|
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||||
Submit your plan with:
|
Submit your plan with:
|
||||||
- approach: High-level strategy
|
- approach: High-level strategy (string)
|
||||||
- steps: List of actionable items
|
- steps: List of step objects with `title` and `description`
|
||||||
- risks: What could go wrong
|
- risks: Optional list of identified risks
|
||||||
- estimated_sessions: How long you think this takes
|
- open_questions: Optional questions that BLOCK starting (must be answered first)
|
||||||
|
|
||||||
### 5. START
|
### 5. START
|
||||||
**Tool:** `roboco_task_start(task_id)`
|
**Tool:** `roboco_task_start(task_id)`
|
||||||
@@ -305,12 +305,17 @@ roboco_task_get("TASK-042")
|
|||||||
# If unclear: ASK in session. Otherwise, proceed silently.
|
# If unclear: ASK in session. Otherwise, proceed silently.
|
||||||
|
|
||||||
# 4. PLAN (required before start!)
|
# 4. PLAN (required before start!)
|
||||||
roboco_task_plan("TASK-042", {
|
roboco_task_plan(
|
||||||
"approach": "Use Redis sliding window counter",
|
"TASK-042",
|
||||||
"steps": ["Add Redis client", "Create decorator", "Apply to auth endpoints", "Tests"],
|
"Use Redis sliding window counter",
|
||||||
"risks": ["Redis config may not exist"],
|
[
|
||||||
"estimated_sessions": 2
|
{"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
|
# 5. START
|
||||||
roboco_task_start("TASK-042")
|
roboco_task_start("TASK-042")
|
||||||
@@ -472,6 +477,7 @@ permissions:
|
|||||||
channels_read:
|
channels_read:
|
||||||
- backend-cell
|
- backend-cell
|
||||||
- dev-all
|
- dev-all
|
||||||
|
- qa-all # Cross-cell QA visibility
|
||||||
- announcements
|
- announcements
|
||||||
- all-hands
|
- 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_scan(team?)` - Find tasks awaiting documentation
|
||||||
- `roboco_task_get(task_id)` - Get task details, dev notes, QA notes
|
- `roboco_task_get(task_id)` - Get task details, dev notes, QA notes
|
||||||
- `roboco_task_claim(task_id)` - Claim for documentation
|
- `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_start(task_id)` - Begin documentation work
|
||||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
- `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)
|
- `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
|
### 3. UNDERSTAND
|
||||||
`roboco_task_get(task_id)` - Read dev notes, QA notes, handoff summary
|
`roboco_task_get(task_id)` - Read dev notes, QA notes, handoff summary
|
||||||
|
|
||||||
### 4. START
|
### 4. PLAN (REQUIRED)
|
||||||
`roboco_task_start(task_id)` - Required before adding progress notes
|
**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
|
### 5. START
|
||||||
- Review commits and code changes
|
**Tool:** `roboco_task_start(task_id)`
|
||||||
- Read dev's journey notes
|
- Move task to "in_progress"
|
||||||
- Check conversation history for context
|
- **REQUIRED** before you can add progress notes
|
||||||
- Understand what was built and why
|
- 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/`:
|
**File Paths** - Write documentation to `/app/docs/`:
|
||||||
- `/app/docs/backend/` - Backend documentation
|
- `/app/docs/backend/` - Backend documentation
|
||||||
- `/app/docs/backend/api/` - API 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)`
|
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
|
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
|
||||||
This sends the task to the Cell PM for final review and completion.
|
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"
|
`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
|
**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()`.
|
and verify all subtasks are done before calling `roboco_task_complete()`.
|
||||||
|
|
||||||
### 8. DOCUMENT
|
### 9. JOURNAL (Optional)
|
||||||
`roboco_journal_reflect(data)` - Document your documentation work
|
`roboco_journal_reflect(data)` - Document your documentation work (YOUR personal journal)
|
||||||
|
|
||||||
### 9. NEXT
|
### 10. NEXT
|
||||||
`roboco_task_scan()` or `roboco_agent_idle()`
|
`roboco_task_scan()` or `roboco_agent_idle()`
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -243,6 +288,8 @@ permissions:
|
|||||||
channels_read:
|
channels_read:
|
||||||
- backend-cell
|
- backend-cell
|
||||||
- doc-all
|
- doc-all
|
||||||
|
- dev-all # Cross-cell dev context for docs
|
||||||
|
- qa-all # Cross-cell QA context for docs
|
||||||
- announcements
|
- announcements
|
||||||
- all-hands
|
- 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_get(task_id)` - Get full task details
|
||||||
- `roboco_task_claim(task_id)` - Claim a task for triage
|
- `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_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_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||||
- `roboco_task_create(data)` - Create subtasks for developers (TaskCreateInput)
|
- `roboco_task_create(data)` - Create subtasks for developers (TaskCreateInput)
|
||||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
- `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
|
- **GATE**: If anything is unclear, ask in #backend-cell or escalate
|
||||||
|
|
||||||
### 4. PLAN
|
### 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:
|
Add your PM assessment as a plan with:
|
||||||
- approach: How this should be broken down or executed
|
- approach: How this should be broken down or executed
|
||||||
- steps: List of subtasks or action items
|
- steps: List of subtasks or action items
|
||||||
@@ -134,6 +134,30 @@ Document your triage decision:
|
|||||||
### 7. DELEGATE
|
### 7. DELEGATE
|
||||||
**This is your main job - assign work to developers!**
|
**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:
|
**For COMPLEX tasks** - Create subtasks:
|
||||||
```python
|
```python
|
||||||
roboco_task_create({
|
roboco_task_create({
|
||||||
@@ -155,10 +179,30 @@ roboco_task_assign("{task_id}", "be-dev-1")
|
|||||||
- `be-dev-1` - Backend Developer 1
|
- `be-dev-1` - Backend Developer 1
|
||||||
- `be-dev-2` - Backend Developer 2
|
- `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:**
|
**CRITICAL RULES:**
|
||||||
- assigned_to MUST be a developer slug, NOT your own ID
|
- assigned_to MUST be a developer slug, NOT your own ID
|
||||||
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
||||||
- Do NOT keep tasks for yourself - delegate to developers!
|
- Do NOT keep tasks for yourself - delegate to developers!
|
||||||
|
- NEVER assign all tasks to one dev - DISTRIBUTE between devs!
|
||||||
|
|
||||||
### 7a. CREATE WORK SESSION (REQUIRED)
|
### 7a. CREATE WORK SESSION (REQUIRED)
|
||||||
**Tool:** `roboco_session_create_for_tasks(data)`
|
**Tool:** `roboco_session_create_for_tasks(data)`
|
||||||
@@ -266,9 +310,9 @@ the task for your final review.
|
|||||||
### Channels You Access
|
### Channels You Access
|
||||||
- **#backend-cell** (read/write) - Your primary workspace
|
- **#backend-cell** (read/write) - Your primary workspace
|
||||||
- **#pm-all** (read/write) - PM coordination
|
- **#pm-all** (read/write) - PM coordination
|
||||||
- **#dev-all** (read) - Dev cross-cell discussion
|
- **#dev-all** (read/write) - Dev cross-cell discussion
|
||||||
- **#qa-all** (read) - QA cross-cell discussion
|
- **#qa-all** (read/write) - QA cross-cell discussion
|
||||||
- **#doc-all** (read) - Documenter cross-cell discussion
|
- **#doc-all** (read/write) - Documenter cross-cell discussion
|
||||||
- **#main-pm-board** (read/write) - Main PM coordination
|
- **#main-pm-board** (read/write) - Main PM coordination
|
||||||
- **#announcements** (read) - Company announcements
|
- **#announcements** (read) - Company announcements
|
||||||
- **#all-hands** (read/write) - Company-wide discussion
|
- **#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 |
|
| Actor | Creates | When |
|
||||||
|-------|---------|------|
|
|-------|---------|------|
|
||||||
| **Cell PM (you)** | Groups in `#backend-cell` | New feature/initiative in your cell |
|
| **Main PM** | Groups in channels | New cross-cell initiatives (escalate if needed) |
|
||||||
| **Cell PM (you)** | Sessions for YOUR parent tasks | Before creating subtasks |
|
| **Cell PM (you)** | Sessions in `#backend-cell` | For parent tasks before creating subtasks |
|
||||||
| **Devs/QA/Doc** | **NOTHING** | Never - they just send with task_id |
|
| **Devs/QA/Doc** | **NOTHING** | Never - they just send with task_id |
|
||||||
|
|
||||||
### Session Inheritance Rule
|
### Session Inheritance Rule
|
||||||
@@ -506,7 +550,9 @@ permissions:
|
|||||||
channels_write:
|
channels_write:
|
||||||
- backend-cell
|
- backend-cell
|
||||||
- pm-all
|
- pm-all
|
||||||
- main-pm-board
|
- dev-all # Cross-cell coordination
|
||||||
|
- qa-all # Cross-cell coordination
|
||||||
|
- doc-all # Cross-cell coordination
|
||||||
- all-hands
|
- all-hands
|
||||||
|
|
||||||
task_permissions:
|
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_scan(team?)` - Find tasks awaiting QA (your review queue)
|
||||||
- `roboco_task_get(task_id)` - Get task details, acceptance criteria, dev notes
|
- `roboco_task_get(task_id)` - Get task details, acceptance criteria, dev notes
|
||||||
- `roboco_task_claim(task_id)` - Claim a task for review
|
- `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_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_progress(task_id, message, percentage)` - Update testing progress (percentage 0-100 required)
|
||||||
- `roboco_task_qa_pass(task_id, qa_notes)` - Approve task (QA only)
|
- `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
|
- **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)`
|
**Tool:** `roboco_task_start(task_id)`
|
||||||
- Move task to "in_progress"
|
- Move task to "in_progress"
|
||||||
- **REQUIRED** before you can add progress notes
|
- **REQUIRED** before you can add progress notes
|
||||||
|
- Will FAIL if you haven't submitted a plan first!
|
||||||
|
|
||||||
### 5. TEST
|
### 6. TEST
|
||||||
Execute thorough testing:
|
Execute thorough testing:
|
||||||
|
|
||||||
**Functional 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)`
|
Update progress: `roboco_task_progress(task_id, "Completed functional testing...", 50)`
|
||||||
Journal findings: `roboco_journal_entry(data)`
|
Journal findings: `roboco_journal_entry(data)`
|
||||||
|
|
||||||
### 6. VERDICT
|
### 7. VERDICT
|
||||||
|
|
||||||
#### PASS
|
#### PASS
|
||||||
**Tool:** `roboco_task_qa_pass(task_id, qa_notes)`
|
**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
|
```python
|
||||||
roboco_task_qa_pass(task_id, {
|
roboco_task_qa_pass(task_id, "All acceptance criteria verified. Edge cases tested.")
|
||||||
"qa_notes": "All acceptance criteria verified. Edge cases tested. Code quality checks pass."
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Tool:** `roboco_message_send(data)`
|
**Tool:** `roboco_message_send(data)`
|
||||||
@@ -163,11 +182,17 @@ roboco_task_qa_pass(task_id, {
|
|||||||
{
|
{
|
||||||
"channel_slug": "backend-cell",
|
"channel_slug": "backend-cell",
|
||||||
"task_id": "{task_id}",
|
"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"
|
"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
|
#### FAIL
|
||||||
**Tool:** `roboco_task_qa_fail(task_id, qa_notes, issues)`
|
**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)`
|
**Tool:** `roboco_journal_reflect(data)`
|
||||||
Document your QA work:
|
|
||||||
|
This is YOUR personal journal - NOT task documentation (Documenter does that).
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"task_id": "{task_id}",
|
"task_id": "{task_id}",
|
||||||
"title": "QA Review: {task title}",
|
"title": "QA Review: {task title}",
|
||||||
"what_done": "Tested functionality, edge cases, security",
|
"what_done": "Tested functionality, edge cases, security",
|
||||||
"what_learned": "Found common pattern for null handling",
|
"what_learned": "Found common pattern for null handling"
|
||||||
"what_struggled": "Test environment setup took time",
|
|
||||||
"next_steps": []
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 8. NEXT
|
### 9. NEXT TASK
|
||||||
After verdict:
|
**Your job on this task is DONE. Move on:**
|
||||||
- `roboco_task_scan()` for next QA task
|
- `roboco_task_scan()` for next QA task
|
||||||
- Or `roboco_agent_idle()` if no more work
|
- 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.
|
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:**
|
**Rule: Check `self_verified` field in task:**
|
||||||
- `roboco_task_submit_pm_review(task_id, notes?)` - Submit your own work for PM review
|
- `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`
|
||||||
**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
|
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|
||||||
@@ -380,6 +415,7 @@ permissions:
|
|||||||
channels_read:
|
channels_read:
|
||||||
- backend-cell
|
- backend-cell
|
||||||
- qa-all
|
- qa-all
|
||||||
|
- dev-all # Cross-cell dev visibility
|
||||||
- announcements
|
- announcements
|
||||||
- all-hands
|
- 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_scan()` - Check for tasks requiring your attention
|
||||||
- `roboco_task_get(task_id)` - Get task details
|
- `roboco_task_get(task_id)` - Get task details
|
||||||
- `roboco_task_claim(task_id)` - Claim a task for triage
|
- `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_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_progress(task_id, message, percentage)` - Update progress (0-100)
|
||||||
- `roboco_task_create(...)` - Create new tasks for cells (pass `status: "backlog"` for setup phase)
|
- `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
|
Push work to cells. Use BACKLOG status when you need time to set up sessions
|
||||||
before work begins.
|
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:**
|
**Standard Distribution Workflow:**
|
||||||
|
|
||||||
**1. CREATE TASKS (with BACKLOG for setup)**
|
**1. CREATE TASKS (with BACKLOG for setup)**
|
||||||
@@ -726,6 +771,7 @@ permissions:
|
|||||||
channels_write:
|
channels_write:
|
||||||
- main-pm-board
|
- main-pm-board
|
||||||
- pm-all
|
- pm-all
|
||||||
|
- dev-all # Cross-cell coordination (sessions, groups)
|
||||||
- announcements
|
- announcements
|
||||||
- all-hands
|
- 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_get(task_id)` - Get full task details with acceptance criteria
|
||||||
- `roboco_task_claim(task_id)` - Claim a pending task
|
- `roboco_task_claim(task_id)` - Claim a pending task
|
||||||
- `roboco_task_start(task_id)` - Begin work (moves to in_progress)
|
- `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_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_block(task_id, reason, blocker_type, what_needed)` - Mark blocked
|
||||||
- `roboco_task_unblock(task_id)` - Resume from blocked state
|
- `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
|
- Do NOT proceed until you understand the acceptance criteria
|
||||||
|
|
||||||
### 4. PLAN
|
### 4. PLAN
|
||||||
**Tool:** `roboco_task_plan(task_id, plan)`
|
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||||
Submit your plan with:
|
Submit your plan with:
|
||||||
- approach: High-level strategy
|
- approach: High-level strategy
|
||||||
- steps: Component breakdown, state management, API integration
|
- steps: Component breakdown, state management, API integration
|
||||||
@@ -382,6 +382,7 @@ permissions:
|
|||||||
channels_read:
|
channels_read:
|
||||||
- frontend-cell
|
- frontend-cell
|
||||||
- dev-all
|
- dev-all
|
||||||
|
- qa-all # Cross-cell QA visibility
|
||||||
- announcements
|
- announcements
|
||||||
- all-hands
|
- 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_scan(team?)` - Find tasks awaiting documentation
|
||||||
- `roboco_task_get(task_id)` - Get task details, dev notes
|
- `roboco_task_get(task_id)` - Get task details, dev notes
|
||||||
- `roboco_task_claim(task_id)` - Claim for documentation
|
- `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_start(task_id)` - Begin documentation work
|
||||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
- `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)
|
- `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
|
### 3. UNDERSTAND
|
||||||
`roboco_task_get(task_id)` - Read dev notes, QA notes, handoff summary
|
`roboco_task_get(task_id)` - Read dev notes, QA notes, handoff summary
|
||||||
|
|
||||||
### 4. START
|
### 4. PLAN (REQUIRED)
|
||||||
`roboco_task_start(task_id)` - Required before adding progress notes
|
**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
|
### 5. START
|
||||||
- Review component code
|
**Tool:** `roboco_task_start(task_id)`
|
||||||
- Read dev's journey notes
|
- Move task to "in_progress"
|
||||||
- Check design specs
|
- **REQUIRED** before you can add progress notes
|
||||||
- Understand usage patterns
|
- 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/`:
|
**File Paths** - Write documentation to `/app/docs/`:
|
||||||
- `/app/docs/frontend/` - Frontend documentation
|
- `/app/docs/frontend/` - Frontend documentation
|
||||||
- `/app/docs/frontend/components/` - Component documentation
|
- `/app/docs/frontend/components/` - Component documentation
|
||||||
@@ -113,7 +158,7 @@ If none: `roboco_agent_idle()`
|
|||||||
- {Description}
|
- {Description}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. SUBMIT TO PM
|
### 8. SUBMIT TO PM
|
||||||
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
|
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
|
||||||
This sends the task to the Cell PM for final review and completion.
|
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"
|
`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
|
**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()`.
|
and verify all subtasks are done before calling `roboco_task_complete()`.
|
||||||
|
|
||||||
### 8. DOCUMENT
|
### 9. JOURNAL (Optional)
|
||||||
`roboco_journal_reflect(data)` - Document your documentation work
|
`roboco_journal_reflect(data)` - Document your documentation work (YOUR personal journal)
|
||||||
|
|
||||||
### 9. NEXT
|
### 10. NEXT
|
||||||
`roboco_task_scan()` or `roboco_agent_idle()`
|
`roboco_task_scan()` or `roboco_agent_idle()`
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -241,6 +286,8 @@ permissions:
|
|||||||
channels_read:
|
channels_read:
|
||||||
- frontend-cell
|
- frontend-cell
|
||||||
- doc-all
|
- doc-all
|
||||||
|
- dev-all # Cross-cell dev context for docs
|
||||||
|
- qa-all # Cross-cell QA context for docs
|
||||||
- announcements
|
- announcements
|
||||||
- all-hands
|
- 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_get(task_id)` - Get full task details
|
||||||
- `roboco_task_claim(task_id)` - Claim a task for triage
|
- `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_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_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||||
- `roboco_task_create(data)` - Create subtasks for developers (TaskCreateInput)
|
- `roboco_task_create(data)` - Create subtasks for developers (TaskCreateInput)
|
||||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
- `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
|
- **GATE**: If anything is unclear, ask in #frontend-cell or escalate
|
||||||
|
|
||||||
### 4. PLAN
|
### 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:
|
Add your PM assessment as a plan with:
|
||||||
- approach: How this should be broken down or executed
|
- approach: How this should be broken down or executed
|
||||||
- steps: List of subtasks or action items
|
- steps: List of subtasks or action items
|
||||||
@@ -136,6 +136,30 @@ Document your triage decision:
|
|||||||
### 7. DELEGATE
|
### 7. DELEGATE
|
||||||
**This is your main job - assign work to developers!**
|
**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:
|
**For COMPLEX tasks** - Create subtasks:
|
||||||
```python
|
```python
|
||||||
roboco_task_create({
|
roboco_task_create({
|
||||||
@@ -157,10 +181,30 @@ roboco_task_assign("{task_id}", "fe-dev-1")
|
|||||||
- `fe-dev-1` - Frontend Developer 1
|
- `fe-dev-1` - Frontend Developer 1
|
||||||
- `fe-dev-2` - Frontend Developer 2
|
- `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:**
|
**CRITICAL RULES:**
|
||||||
- assigned_to MUST be a developer slug, NOT your own ID
|
- assigned_to MUST be a developer slug, NOT your own ID
|
||||||
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
||||||
- Do NOT keep tasks for yourself - delegate to developers!
|
- Do NOT keep tasks for yourself - delegate to developers!
|
||||||
|
- NEVER assign all tasks to one dev - DISTRIBUTE between devs!
|
||||||
|
|
||||||
### 7a. CREATE WORK SESSION (REQUIRED)
|
### 7a. CREATE WORK SESSION (REQUIRED)
|
||||||
**Tool:** `roboco_session_create_for_tasks(data)`
|
**Tool:** `roboco_session_create_for_tasks(data)`
|
||||||
@@ -280,9 +324,9 @@ Can these be added?
|
|||||||
### Channels You Access
|
### Channels You Access
|
||||||
- **#frontend-cell** (read/write) - Your primary workspace
|
- **#frontend-cell** (read/write) - Your primary workspace
|
||||||
- **#pm-all** (read/write) - PM coordination
|
- **#pm-all** (read/write) - PM coordination
|
||||||
- **#dev-all** (read) - Dev cross-cell discussion
|
- **#dev-all** (read/write) - Dev cross-cell discussion
|
||||||
- **#qa-all** (read) - QA cross-cell discussion
|
- **#qa-all** (read/write) - QA cross-cell discussion
|
||||||
- **#doc-all** (read) - Documenter cross-cell discussion
|
- **#doc-all** (read/write) - Documenter cross-cell discussion
|
||||||
- **#main-pm-board** (read/write) - Main PM coordination
|
- **#main-pm-board** (read/write) - Main PM coordination
|
||||||
- **#announcements** (read) - Company announcements
|
- **#announcements** (read) - Company announcements
|
||||||
- **#all-hands** (read/write) - Company-wide discussion
|
- **#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 |
|
| Actor | Creates | When |
|
||||||
|-------|---------|------|
|
|-------|---------|------|
|
||||||
| **Cell PM (you)** | Groups in `#frontend-cell` | New feature/initiative in your cell |
|
| **Main PM** | Groups in channels | New cross-cell initiatives (escalate if needed) |
|
||||||
| **Cell PM (you)** | Sessions for YOUR parent tasks | Before creating subtasks |
|
| **Cell PM (you)** | Sessions in `#frontend-cell` | For parent tasks before creating subtasks |
|
||||||
| **Devs/QA/Doc** | **NOTHING** | Never - they just send with task_id |
|
| **Devs/QA/Doc** | **NOTHING** | Never - they just send with task_id |
|
||||||
|
|
||||||
### Session Inheritance Rule
|
### Session Inheritance Rule
|
||||||
@@ -507,7 +551,9 @@ permissions:
|
|||||||
channels_write:
|
channels_write:
|
||||||
- frontend-cell
|
- frontend-cell
|
||||||
- pm-all
|
- pm-all
|
||||||
- main-pm-board
|
- dev-all # Cross-cell coordination
|
||||||
|
- qa-all # Cross-cell coordination
|
||||||
|
- doc-all # Cross-cell coordination
|
||||||
- all-hands
|
- all-hands
|
||||||
|
|
||||||
task_permissions:
|
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_scan(team?)` - Find tasks awaiting QA
|
||||||
- `roboco_task_get(task_id)` - Get task details
|
- `roboco_task_get(task_id)` - Get task details
|
||||||
- `roboco_task_claim(task_id)` - Claim for review
|
- `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_start(task_id)` - Begin QA work
|
||||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||||
- `roboco_task_qa_pass(task_id, qa_notes)` - Approve task
|
- `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.
|
If dev_notes is empty, that's a valid FAIL reason.
|
||||||
|
|
||||||
### 4. START
|
### 4. PLAN (REQUIRED)
|
||||||
`roboco_task_start(task_id)` - Required before adding progress notes
|
**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**
|
**Visual Testing**
|
||||||
- Matches design specs exactly
|
- Matches design specs exactly
|
||||||
- All states render correctly
|
- 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)`
|
Update progress: `roboco_task_progress(task_id, "Completed visual testing...", 50)`
|
||||||
|
|
||||||
### 6. VERDICT
|
### 7. VERDICT
|
||||||
**PASS:** `roboco_task_qa_pass(task_id, qa_notes)`
|
|
||||||
**FAIL:** `roboco_task_qa_fail(task_id, qa_notes, issues)`
|
|
||||||
|
|
||||||
### 7. DOCUMENT
|
#### PASS
|
||||||
`roboco_journal_reflect(data)` - Document your QA work
|
**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()`
|
`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.
|
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:**
|
**Rule: Check `self_verified` field in task:**
|
||||||
- `roboco_task_submit_pm_review(task_id, notes?)` - Submit your own work for PM review
|
- `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`
|
||||||
**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
|
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|
||||||
@@ -261,6 +318,7 @@ permissions:
|
|||||||
channels_read:
|
channels_read:
|
||||||
- frontend-cell
|
- frontend-cell
|
||||||
- qa-all
|
- qa-all
|
||||||
|
- dev-all # Cross-cell dev visibility
|
||||||
- announcements
|
- announcements
|
||||||
- all-hands
|
- 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_get(task_id)` - Get full task details with requirements
|
||||||
- `roboco_task_claim(task_id)` - Claim a pending task
|
- `roboco_task_claim(task_id)` - Claim a pending task
|
||||||
- `roboco_task_start(task_id)` - Begin work (moves to in_progress)
|
- `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_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_block(task_id, reason, blocker_type, what_needed)` - Mark blocked
|
||||||
- `roboco_task_unblock(task_id)` - Resume from blocked state
|
- `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
|
- Do NOT proceed until you understand what success looks like
|
||||||
|
|
||||||
### 4. PLAN
|
### 4. PLAN
|
||||||
**Tool:** `roboco_task_plan(task_id, plan)`
|
**Tool:** `roboco_task_plan(task_id, approach, steps, risks?, open_questions?)`
|
||||||
Submit your plan with:
|
Submit your plan with:
|
||||||
- approach: Design strategy
|
- approach: Design strategy
|
||||||
- steps: Components needed, states to cover, breakpoints
|
- steps: Components needed, states to cover, breakpoints
|
||||||
@@ -350,11 +350,13 @@ permissions:
|
|||||||
channels_read:
|
channels_read:
|
||||||
- uxui-cell
|
- uxui-cell
|
||||||
- dev-all
|
- dev-all
|
||||||
|
- qa-all # Cross-cell QA visibility
|
||||||
- announcements
|
- announcements
|
||||||
- all-hands
|
- all-hands
|
||||||
|
|
||||||
channels_write:
|
channels_write:
|
||||||
- uxui-cell
|
- uxui-cell
|
||||||
|
- dev-all # Cross-cell dev coordination
|
||||||
- all-hands
|
- all-hands
|
||||||
|
|
||||||
task_permissions:
|
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_scan(team?)` - Find tasks awaiting documentation
|
||||||
- `roboco_task_get(task_id)` - Get task details, design notes
|
- `roboco_task_get(task_id)` - Get task details, design notes
|
||||||
- `roboco_task_claim(task_id)` - Claim for documentation
|
- `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_start(task_id)` - Begin documentation work
|
||||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
- `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)
|
- `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
|
### 3. UNDERSTAND
|
||||||
`roboco_task_get(task_id)` - Read design notes, QA notes, handoff summary
|
`roboco_task_get(task_id)` - Read design notes, QA notes, handoff summary
|
||||||
|
|
||||||
### 4. START
|
### 4. PLAN (REQUIRED)
|
||||||
`roboco_task_start(task_id)` - Required before adding progress notes
|
**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
|
### 5. START
|
||||||
- Review Figma files
|
**Tool:** `roboco_task_start(task_id)`
|
||||||
- Read designer's journey notes
|
- Move task to "in_progress"
|
||||||
- Check design decisions made
|
- **REQUIRED** before you can add progress notes
|
||||||
- Understand usage guidelines
|
- 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/`:
|
**File Paths** - Write documentation to `/app/docs/`:
|
||||||
- `/app/docs/ux_ui/` - UX/UI documentation
|
- `/app/docs/ux_ui/` - UX/UI documentation
|
||||||
- `/app/docs/ux_ui/design-system/` - Design system documentation
|
- `/app/docs/ux_ui/design-system/` - Design system documentation
|
||||||
@@ -113,7 +157,7 @@ If none: `roboco_agent_idle()`
|
|||||||
- {Description}
|
- {Description}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. SUBMIT TO PM
|
### 8. SUBMIT TO PM
|
||||||
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
|
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
|
||||||
This sends the task to the Cell PM for final review and completion.
|
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"
|
`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
|
**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()`.
|
and verify all subtasks are done before calling `roboco_task_complete()`.
|
||||||
|
|
||||||
### 8. DOCUMENT
|
### 9. JOURNAL (Optional)
|
||||||
`roboco_journal_reflect(data)` - Document your documentation work
|
`roboco_journal_reflect(data)` - Document your documentation work (YOUR personal journal)
|
||||||
|
|
||||||
### 9. NEXT
|
### 10. NEXT
|
||||||
`roboco_task_scan()` or `roboco_agent_idle()`
|
`roboco_task_scan()` or `roboco_agent_idle()`
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -241,6 +285,8 @@ permissions:
|
|||||||
channels_read:
|
channels_read:
|
||||||
- uxui-cell
|
- uxui-cell
|
||||||
- doc-all
|
- doc-all
|
||||||
|
- dev-all # Cross-cell dev context for docs
|
||||||
|
- qa-all # Cross-cell QA context for docs
|
||||||
- announcements
|
- announcements
|
||||||
- all-hands
|
- 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_get(task_id)` - Get full task details
|
||||||
- `roboco_task_claim(task_id)` - Claim a task for triage
|
- `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_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_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||||
- `roboco_task_create(data)` - Create subtasks for designers (TaskCreateInput)
|
- `roboco_task_create(data)` - Create subtasks for designers (TaskCreateInput)
|
||||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
- `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
|
- **GATE**: If anything is unclear, ask in #uxui-cell or escalate
|
||||||
|
|
||||||
### 4. PLAN
|
### 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:
|
Add your PM assessment as a plan with:
|
||||||
- approach: How this should be broken down or executed
|
- approach: How this should be broken down or executed
|
||||||
- steps: List of subtasks or action items
|
- steps: List of subtasks or action items
|
||||||
@@ -137,6 +137,29 @@ Document your triage decision:
|
|||||||
### 7. DELEGATE
|
### 7. DELEGATE
|
||||||
**This is your main job - assign work to designers!**
|
**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:
|
**For COMPLEX tasks** - Create subtasks:
|
||||||
```python
|
```python
|
||||||
roboco_task_create({
|
roboco_task_create({
|
||||||
@@ -280,9 +303,9 @@ UX-PM: @ProductOwner Question on TASK-055:
|
|||||||
### Channels You Access
|
### Channels You Access
|
||||||
- **#uxui-cell** (read/write) - Your primary workspace
|
- **#uxui-cell** (read/write) - Your primary workspace
|
||||||
- **#pm-all** (read/write) - PM coordination
|
- **#pm-all** (read/write) - PM coordination
|
||||||
- **#dev-all** (read) - Dev cross-cell discussion
|
- **#dev-all** (read/write) - Dev cross-cell discussion
|
||||||
- **#qa-all** (read) - QA cross-cell discussion
|
- **#qa-all** (read/write) - QA cross-cell discussion
|
||||||
- **#doc-all** (read) - Documenter cross-cell discussion
|
- **#doc-all** (read/write) - Documenter cross-cell discussion
|
||||||
- **#main-pm-board** (read/write) - Main PM coordination
|
- **#main-pm-board** (read/write) - Main PM coordination
|
||||||
- **#announcements** (read) - Company announcements
|
- **#announcements** (read) - Company announcements
|
||||||
- **#all-hands** (read/write) - Company-wide discussion
|
- **#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 |
|
| Actor | Creates | When |
|
||||||
|-------|---------|------|
|
|-------|---------|------|
|
||||||
| **Cell PM (you)** | Groups in `#uxui-cell` | New feature/initiative in your cell |
|
| **Main PM** | Groups in channels | New cross-cell initiatives (escalate if needed) |
|
||||||
| **Cell PM (you)** | Sessions for YOUR parent tasks | Before creating subtasks |
|
| **Cell PM (you)** | Sessions in `#uxui-cell` | For parent tasks before creating subtasks |
|
||||||
| **Devs/QA/Doc** | **NOTHING** | Never - they just send with task_id |
|
| **Devs/QA/Doc** | **NOTHING** | Never - they just send with task_id |
|
||||||
|
|
||||||
### Session Inheritance Rule
|
### Session Inheritance Rule
|
||||||
@@ -512,7 +535,9 @@ permissions:
|
|||||||
channels_write:
|
channels_write:
|
||||||
- uxui-cell
|
- uxui-cell
|
||||||
- pm-all
|
- pm-all
|
||||||
- main-pm-board
|
- dev-all # Cross-cell coordination
|
||||||
|
- qa-all # Cross-cell coordination
|
||||||
|
- doc-all # Cross-cell coordination
|
||||||
- all-hands
|
- all-hands
|
||||||
|
|
||||||
task_permissions:
|
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_scan(team?)` - Find tasks awaiting QA
|
||||||
- `roboco_task_get(task_id)` - Get task details
|
- `roboco_task_get(task_id)` - Get task details
|
||||||
- `roboco_task_claim(task_id)` - Claim for review
|
- `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_start(task_id)` - Begin QA work
|
||||||
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||||
- `roboco_task_qa_pass(task_id, qa_notes)` - Approve design
|
- `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.
|
If dev_notes is empty or no Figma link provided, that's a valid FAIL reason.
|
||||||
|
|
||||||
### 4. START
|
### 4. PLAN (REQUIRED)
|
||||||
`roboco_task_start(task_id)` - Required before adding notes
|
**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**
|
**Completeness**
|
||||||
- All required states designed
|
- All required states designed
|
||||||
- All breakpoints covered
|
- 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
|
- Assets exportable
|
||||||
- Notes for frontend clear
|
- Notes for frontend clear
|
||||||
|
|
||||||
### 6. VERDICT
|
### 7. VERDICT
|
||||||
**PASS:** `roboco_task_qa_pass(task_id, qa_notes)`
|
|
||||||
**FAIL:** `roboco_task_qa_fail(task_id, qa_notes, issues)`
|
|
||||||
|
|
||||||
### 7. DOCUMENT
|
#### PASS
|
||||||
`roboco_journal_reflect(data)` - Document your review
|
**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()`
|
`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.
|
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:**
|
**Rule: Check `self_verified` field in task:**
|
||||||
- `roboco_task_submit_pm_review(task_id, notes?)` - Submit your own work for PM review
|
- `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`
|
||||||
**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
|
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|
||||||
@@ -259,6 +316,7 @@ permissions:
|
|||||||
channels_read:
|
channels_read:
|
||||||
- uxui-cell
|
- uxui-cell
|
||||||
- qa-all
|
- qa-all
|
||||||
|
- dev-all # Cross-cell dev visibility
|
||||||
- announcements
|
- announcements
|
||||||
- all-hands
|
- 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-qa": "frontend",
|
||||||
"fe-pm": "frontend",
|
"fe-pm": "frontend",
|
||||||
"fe-doc": "frontend",
|
"fe-doc": "frontend",
|
||||||
# UX/UI cell
|
# UX/UI cell (matches Team.UX_UI = "ux_ui")
|
||||||
"ux-dev": "uxui",
|
"ux-dev": "ux_ui",
|
||||||
"ux-qa": "uxui",
|
"ux-qa": "ux_ui",
|
||||||
"ux-pm": "uxui",
|
"ux-pm": "ux_ui",
|
||||||
"ux-doc": "uxui",
|
"ux-doc": "ux_ui",
|
||||||
# Management has no team
|
# Management has no team
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ AGENT_TEAM_MAP: Final[dict[str, str]] = {
|
|||||||
CELL_MEMBERS: Final[dict[str, list[str]]] = {
|
CELL_MEMBERS: Final[dict[str, list[str]]] = {
|
||||||
"backend": ["be-dev-1", "be-dev-2", "be-qa", "be-pm", "be-doc"],
|
"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"],
|
"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
|
||||||
ALL_PMS: Final[list[str]] = ["be-pm", "fe-pm", "ux-pm", "main-pm"]
|
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-capable roles (can create and assign tasks)
|
||||||
PM_ROLES: Final[set[str]] = {
|
PM_ROLES: Final[set[str]] = {
|
||||||
"cell_pm",
|
"cell_pm",
|
||||||
@@ -248,41 +254,43 @@ def get_escalation_target(agent_id: str) -> str | None:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
CHANNEL_ACCESS: Final[dict[str, dict[str, list[str]]]] = {
|
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": {
|
"backend-cell": {
|
||||||
"read": CELL_MEMBERS["backend"],
|
"read": [*CELL_MEMBERS["backend"], "main-pm"],
|
||||||
"write": CELL_MEMBERS["backend"],
|
"write": CELL_MEMBERS["backend"],
|
||||||
"silent": ["auditor"],
|
"silent": ["auditor"],
|
||||||
},
|
},
|
||||||
"frontend-cell": {
|
"frontend-cell": {
|
||||||
"read": CELL_MEMBERS["frontend"],
|
"read": [*CELL_MEMBERS["frontend"], "main-pm"],
|
||||||
"write": CELL_MEMBERS["frontend"],
|
"write": CELL_MEMBERS["frontend"],
|
||||||
"silent": ["auditor"],
|
"silent": ["auditor"],
|
||||||
},
|
},
|
||||||
"uxui-cell": {
|
"uxui-cell": {
|
||||||
"read": CELL_MEMBERS["uxui"],
|
"read": [*CELL_MEMBERS["ux_ui"], "main-pm"],
|
||||||
"write": CELL_MEMBERS["uxui"],
|
"write": CELL_MEMBERS["ux_ui"],
|
||||||
"silent": ["auditor"],
|
"silent": ["auditor"],
|
||||||
},
|
},
|
||||||
# Cross-cell role channels
|
# Cross-cell role channels
|
||||||
|
# Cell members read/write their role channel
|
||||||
|
# Cell PMs read/write ALL cross-cell channels for coordination
|
||||||
"dev-all": {
|
"dev-all": {
|
||||||
"read": ["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": ["be-dev-1", "be-dev-2", "fe-dev-1", "fe-dev-2", "ux-dev"],
|
"write": [*ALL_DEVS, *CELL_PMS, "main-pm"],
|
||||||
"silent": ["auditor"],
|
"silent": ["auditor"],
|
||||||
},
|
},
|
||||||
"qa-all": {
|
"qa-all": {
|
||||||
"read": ["be-qa", "fe-qa", "ux-qa"],
|
"read": [*ALL_QA, *ALL_DEVS, *ALL_DOCS, *CELL_PMS, "main-pm"],
|
||||||
"write": ["be-qa", "fe-qa", "ux-qa"],
|
"write": [*ALL_QA, *CELL_PMS],
|
||||||
"silent": ["auditor"],
|
"silent": ["auditor"],
|
||||||
},
|
},
|
||||||
"pm-all": {
|
"pm-all": {
|
||||||
"read": ["be-pm", "fe-pm", "ux-pm", "main-pm"],
|
"read": [*CELL_PMS, "main-pm"],
|
||||||
"write": ["be-pm", "fe-pm", "ux-pm", "main-pm"],
|
"write": [*CELL_PMS, "main-pm"],
|
||||||
"silent": ["auditor"],
|
"silent": ["auditor"],
|
||||||
},
|
},
|
||||||
"doc-all": {
|
"doc-all": {
|
||||||
"read": ["be-doc", "fe-doc", "ux-doc"],
|
"read": [*ALL_DOCS, *CELL_PMS, "main-pm"],
|
||||||
"write": ["be-doc", "fe-doc", "ux-doc"],
|
"write": [*ALL_DOCS, *CELL_PMS],
|
||||||
"silent": ["auditor"],
|
"silent": ["auditor"],
|
||||||
},
|
},
|
||||||
# Management channels
|
# Management channels
|
||||||
@@ -292,7 +300,7 @@ CHANNEL_ACCESS: Final[dict[str, dict[str, list[str]]]] = {
|
|||||||
"silent": [],
|
"silent": [],
|
||||||
},
|
},
|
||||||
"board-private": {
|
"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"],
|
"write": ["product-owner", "head-marketing", "auditor", "ceo"],
|
||||||
"silent": [],
|
"silent": [],
|
||||||
},
|
},
|
||||||
|
|||||||
+22
-22
@@ -40,7 +40,7 @@ from roboco.api.schemas.tasks import (
|
|||||||
transform_update_data,
|
transform_update_data,
|
||||||
)
|
)
|
||||||
from roboco.db.tables import AgentTable, NotificationTable
|
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.models.task import TaskCreate
|
||||||
from roboco.services.audit import get_audit_service
|
from roboco.services.audit import get_audit_service
|
||||||
from roboco.services.messaging import get_messaging_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",
|
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:
|
if not task:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
@@ -544,9 +545,9 @@ async def block_task(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Only assigned agent or PM can block a task
|
# Only assigned agent or PM can block a task
|
||||||
if task.assigned_to != agent.agent_id and agent.role.value not in (
|
if task.assigned_to != agent.agent_id and agent.role not in (
|
||||||
"cell_pm",
|
AgentRole.CELL_PM,
|
||||||
"main_pm",
|
AgentRole.MAIN_PM,
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
@@ -587,9 +588,9 @@ async def soft_block_task(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Only assigned agent or PM can block a task
|
# Only assigned agent or PM can block a task
|
||||||
if task.assigned_to != agent.agent_id and agent.role.value not in (
|
if task.assigned_to != agent.agent_id and agent.role not in (
|
||||||
"cell_pm",
|
AgentRole.CELL_PM,
|
||||||
"main_pm",
|
AgentRole.MAIN_PM,
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
@@ -623,9 +624,9 @@ async def unblock_task(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Only assigned agent or PM can unblock a task
|
# Only assigned agent or PM can unblock a task
|
||||||
if task.assigned_to != agent.agent_id and agent.role.value not in (
|
if task.assigned_to != agent.agent_id and agent.role not in (
|
||||||
"cell_pm",
|
AgentRole.CELL_PM,
|
||||||
"main_pm",
|
AgentRole.MAIN_PM,
|
||||||
):
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
@@ -782,7 +783,7 @@ async def pass_qa(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Only QA agents can pass/fail QA
|
# Only QA agents can pass/fail QA
|
||||||
if agent.role.value != "qa":
|
if agent.role != AgentRole.QA:
|
||||||
audit = get_audit_service()
|
audit = get_audit_service()
|
||||||
await audit.log_task_action_denial(
|
await audit.log_task_action_denial(
|
||||||
agent_id=agent.agent_id,
|
agent_id=agent.agent_id,
|
||||||
@@ -819,7 +820,7 @@ async def pass_qa(
|
|||||||
if not task:
|
if not task:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
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()
|
await db.commit()
|
||||||
return task_to_response(task)
|
return task_to_response(task)
|
||||||
@@ -841,7 +842,7 @@ async def fail_qa(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Only QA agents can pass/fail QA
|
# Only QA agents can pass/fail QA
|
||||||
if agent.role.value != "qa":
|
if agent.role != AgentRole.QA:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Only QA agents can fail QA reviews",
|
detail="Only QA agents can fail QA reviews",
|
||||||
@@ -861,7 +862,7 @@ async def fail_qa(
|
|||||||
if not task:
|
if not task:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
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()
|
await db.commit()
|
||||||
return task_to_response(task)
|
return task_to_response(task)
|
||||||
@@ -887,7 +888,7 @@ async def docs_complete(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Only documenter role can mark docs complete
|
# Only documenter role can mark docs complete
|
||||||
if agent.role.value != "documenter":
|
if agent.role != AgentRole.DOCUMENTER:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Only documenters can mark documentation as complete",
|
detail="Only documenters can mark documentation as complete",
|
||||||
@@ -914,7 +915,7 @@ async def docs_complete(
|
|||||||
if not task:
|
if not task:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
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()
|
await db.commit()
|
||||||
return task_to_response(task)
|
return task_to_response(task)
|
||||||
@@ -1119,11 +1120,10 @@ async def escalate_task(
|
|||||||
detail=f"Escalation target not found: {target_slug}",
|
detail=f"Escalation target not found: {target_slug}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create escalation notification directly (bypassing permission checks)
|
# Create escalation notification directly
|
||||||
body = (
|
# NOTE: Permission is enforced via get_escalation_target() which constrains
|
||||||
f"Task {task_id} escalated by {agent_record.slug}.\n\n"
|
# the escalation chain (devs→PM, PM→MainPM, etc.) per agents_config
|
||||||
f"Reason: {data.reason}"
|
body = f"Task {task_id} escalated by {agent_record.slug}.\n\nReason: {data.reason}"
|
||||||
)
|
|
||||||
notification = NotificationTable(
|
notification = NotificationTable(
|
||||||
type="blocker_escalation",
|
type="blocker_escalation",
|
||||||
priority="high",
|
priority="high",
|
||||||
|
|||||||
+46
-3
@@ -5,6 +5,11 @@ Real-time communication via WebSocket connections for:
|
|||||||
- Channel streams (all messages in a channel)
|
- Channel streams (all messages in a channel)
|
||||||
- Agent streams (individual agent output)
|
- Agent streams (individual agent output)
|
||||||
- Session streams (messages in a session)
|
- 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
|
import asyncio
|
||||||
@@ -20,6 +25,8 @@ from roboco.api.schemas.websocket import (
|
|||||||
NewMessageBroadcast,
|
NewMessageBroadcast,
|
||||||
)
|
)
|
||||||
from roboco.config import settings
|
from roboco.config import settings
|
||||||
|
from roboco.db.base import get_db
|
||||||
|
from roboco.services.repositories import resolve_agent_uuid
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -177,6 +184,24 @@ class ConnectionManager:
|
|||||||
manager = 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:
|
async def validate_channel_access(channel_id: UUID, agent_id: UUID) -> bool:
|
||||||
"""
|
"""
|
||||||
Validate that an agent has access to a channel.
|
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)
|
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||||
return
|
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)
|
await manager.connect_agent(websocket, agent_id, viewer_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -327,6 +357,11 @@ async def session_stream(
|
|||||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||||
return
|
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)
|
await manager.connect_session(websocket, session_id, agent_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -356,6 +391,11 @@ async def notification_stream(
|
|||||||
|
|
||||||
Agents receive real-time notifications via this 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)
|
await manager.connect_notifications(websocket, agent_id)
|
||||||
|
|
||||||
try:
|
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."""
|
"""Broadcast an agent stream chunk to watchers."""
|
||||||
event = {
|
event = {
|
||||||
"type": "agent.stream",
|
"type": "agent.stream",
|
||||||
"agent_id": str(agent_id),
|
"agent_id": agent_id,
|
||||||
"chunk": chunk,
|
"chunk": chunk,
|
||||||
"timestamp": datetime.now(UTC).isoformat(),
|
"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(
|
async def broadcast_session_closed(
|
||||||
|
|||||||
+1
-2
@@ -11,13 +11,12 @@ from pathlib import Path
|
|||||||
import structlog
|
import structlog
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from roboco.agents import set_reasoning_stream_callback
|
|
||||||
from roboco.api.deps import set_orchestrator
|
from roboco.api.deps import set_orchestrator
|
||||||
from roboco.api.websocket import broadcast_agent_chunk
|
from roboco.api.websocket import broadcast_agent_chunk
|
||||||
from roboco.config import settings
|
from roboco.config import settings
|
||||||
from roboco.db import bootstrap_database
|
from roboco.db import bootstrap_database
|
||||||
from roboco.events import EventBus, register_default_handlers, set_event_context
|
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
|
from roboco.services.notification import NotificationService
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
logger = structlog.get_logger()
|
||||||
|
|||||||
+52
-24
@@ -93,10 +93,10 @@ class AgentTable(Base):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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(
|
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
|
# Relationships - use lazy="joined" for single optional relationship
|
||||||
@@ -156,10 +156,10 @@ class TaskTable(Base):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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(
|
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(
|
claimed_at: Mapped[datetime | None] = mapped_column(
|
||||||
DateTime(timezone=True), nullable=True
|
DateTime(timezone=True), nullable=True
|
||||||
@@ -279,10 +279,10 @@ class ChannelTable(Base):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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(
|
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
|
# Relationships - use lazy="select" for collections to avoid N+1
|
||||||
@@ -340,10 +340,10 @@ class GroupTable(Base):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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(
|
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
|
# Relationships - use lazy="select" for collections to avoid N+1
|
||||||
@@ -402,10 +402,10 @@ class SessionTable(Base):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
started_at: Mapped[datetime] = mapped_column(
|
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(
|
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(
|
closed_at: Mapped[datetime | None] = mapped_column(
|
||||||
DateTime(timezone=True), nullable=True
|
DateTime(timezone=True), nullable=True
|
||||||
@@ -417,7 +417,7 @@ class SessionTable(Base):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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+)
|
# Relationships - CRITICAL: use lazy="select" for messages (sessions can have 100+)
|
||||||
@@ -480,7 +480,7 @@ class SessionTaskTable(Base):
|
|||||||
|
|
||||||
# Audit
|
# Audit
|
||||||
added_at: Mapped[datetime] = mapped_column(
|
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(
|
added_by: Mapped[UUID] = mapped_column(
|
||||||
UUID(as_uuid=True),
|
UUID(as_uuid=True),
|
||||||
@@ -585,7 +585,10 @@ class MessageTable(Base):
|
|||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
timestamp: Mapped[datetime] = mapped_column(
|
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
|
# Extraction metadata
|
||||||
@@ -600,7 +603,7 @@ class MessageTable(Base):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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
|
# Relationships - use lazy="joined" for agent to avoid N+1 on message lists
|
||||||
@@ -670,7 +673,10 @@ class NotificationTable(Base):
|
|||||||
|
|
||||||
# Timing
|
# Timing
|
||||||
timestamp: Mapped[datetime] = mapped_column(
|
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(
|
expires_at: Mapped[datetime | None] = mapped_column(
|
||||||
DateTime(timezone=True), nullable=True
|
DateTime(timezone=True), nullable=True
|
||||||
@@ -688,7 +694,7 @@ class NotificationTable(Base):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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
|
# Relationships
|
||||||
@@ -741,10 +747,10 @@ class JournalTable(Base):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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(
|
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
|
# Relationships - use lazy="select" for entries collection to avoid N+1
|
||||||
@@ -789,7 +795,10 @@ class JournalEntryTable(Base):
|
|||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
timestamp: Mapped[datetime] = mapped_column(
|
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)
|
tags: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
|
||||||
|
|
||||||
@@ -801,10 +810,10 @@ class JournalEntryTable(Base):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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(
|
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
|
# Relationships
|
||||||
@@ -826,7 +835,26 @@ class JournalEntryTable(Base):
|
|||||||
|
|
||||||
|
|
||||||
class HandoffTable(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"
|
__tablename__ = "handoffs"
|
||||||
|
|
||||||
@@ -895,10 +923,10 @@ class HandoffTable(Base):
|
|||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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(
|
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(
|
claimed_at: Mapped[datetime | None] = mapped_column(
|
||||||
DateTime(timezone=True), nullable=True
|
DateTime(timezone=True), nullable=True
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ Provides rule enforcement for all RoboCo operations:
|
|||||||
- Message validation
|
- Message validation
|
||||||
- Session boundaries
|
- Session boundaries
|
||||||
- Handoff requirements
|
- 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 (
|
from roboco.enforcement.channel_access import (
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ __all__ = ["VALID_TRANSITIONS", "TaskLifecycleError", "validate_task_transition"
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
VALID_TRANSITIONS: dict[str, list[str]] = {
|
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"],
|
"pending": ["claimed", "cancelled"],
|
||||||
# Claimed - can start, unclaim, or cancel
|
# Claimed - can start, unclaim, or cancel
|
||||||
"claimed": ["in_progress", "pending", "cancelled"],
|
"claimed": ["in_progress", "pending", "cancelled"],
|
||||||
# In progress - can block, pause, submit for verification, or cancel
|
# In progress - can block, pause, verify, complete (PM only), or cancel
|
||||||
"in_progress": ["blocked", "paused", "verifying", "cancelled"],
|
"in_progress": ["blocked", "paused", "verifying", "completed", "cancelled"],
|
||||||
# Blocked - can unblock back to in_progress or cancel
|
# Blocked - can unblock back to in_progress or cancel
|
||||||
"blocked": ["in_progress", "cancelled"],
|
"blocked": ["in_progress", "cancelled"],
|
||||||
# Paused - can resume back to in_progress or cancel
|
# 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 - back to work or cancel
|
||||||
"needs_revision": ["in_progress", "cancelled"],
|
"needs_revision": ["in_progress", "cancelled"],
|
||||||
# Awaiting QA - can pass (to docs), fail (needs revision), block, or cancel
|
# Awaiting QA - QA claims, passes, fails, or blocks
|
||||||
"awaiting_qa": ["awaiting_documentation", "needs_revision", "blocked", "cancelled"],
|
"awaiting_qa": [
|
||||||
# Awaiting documentation - documenter marks docs done, goes to PM review
|
"claimed",
|
||||||
"awaiting_documentation": ["awaiting_pm_review", "cancelled"],
|
"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 - PM reviews and completes, or cancels
|
||||||
"awaiting_pm_review": ["completed", "cancelled"],
|
"awaiting_pm_review": ["completed", "cancelled"],
|
||||||
# Terminal states - cannot transition out
|
# Terminal states - cannot transition out
|
||||||
@@ -56,14 +64,20 @@ _CANCEL_ROLES = ["cell_pm", "main_pm", "product_owner", "head_marketing"]
|
|||||||
|
|
||||||
# Transitions that require specific roles
|
# Transitions that require specific roles
|
||||||
ROLE_RESTRICTED_TRANSITIONS: dict[tuple[str, str], list[str]] = {
|
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", "awaiting_documentation"): ["qa"],
|
||||||
("awaiting_qa", "needs_revision"): ["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"],
|
("awaiting_documentation", "awaiting_pm_review"): ["documenter"],
|
||||||
# Only PM can complete after PM review
|
# Only PM can complete tasks (either after PM review or their own work)
|
||||||
("awaiting_pm_review", "completed"): _CANCEL_ROLES, # PMs complete tasks
|
("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)
|
# Only PM or higher can cancel tasks (all states that allow cancel)
|
||||||
|
("backlog", "cancelled"): _CANCEL_ROLES,
|
||||||
("pending", "cancelled"): _CANCEL_ROLES,
|
("pending", "cancelled"): _CANCEL_ROLES,
|
||||||
("claimed", "cancelled"): _CANCEL_ROLES,
|
("claimed", "cancelled"): _CANCEL_ROLES,
|
||||||
("in_progress", "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)
|
3. AVAILABLE tasks (team pool, can claim)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
team: Optional team filter (backend, frontend, uxui)
|
team: Optional team filter (backend, frontend, ux_ui)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with paused/assigned/available tasks and guidance
|
Dict with paused/assigned/available tasks and guidance
|
||||||
@@ -406,9 +406,7 @@ def _register_developer_submit_tools(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _register_qa_verdict_tools(
|
def _register_qa_verdict_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||||
mcp: FastMCP, client: ApiClient, agent_id: str
|
|
||||||
) -> None:
|
|
||||||
"""Register QA-only verdict tools (qa_pass, qa_fail)."""
|
"""Register QA-only verdict tools (qa_pass, qa_fail)."""
|
||||||
|
|
||||||
@mcp.tool()
|
@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)
|
return await handle_task_qa_fail(client, task_id, qa_notes, issues, agent_id)
|
||||||
|
|
||||||
|
|
||||||
def _register_documenter_tools(
|
def _register_documenter_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||||
mcp: FastMCP, client: ApiClient, agent_id: str
|
|
||||||
) -> None:
|
|
||||||
"""Register documenter-only tools (docs_complete)."""
|
"""Register documenter-only tools (docs_complete)."""
|
||||||
|
|
||||||
@mcp.tool()
|
@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)
|
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()
|
@mcp.tool()
|
||||||
async def roboco_group_create(data: GroupCreateInput) -> dict[str, Any]:
|
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
|
# Documenters: docs completion only
|
||||||
_register_documenter_tools(mcp, client, agent_id)
|
_register_documenter_tools(mcp, client, agent_id)
|
||||||
|
|
||||||
elif role in ("cell_pm", "main_pm"):
|
elif role == "cell_pm":
|
||||||
# PMs: full management capabilities
|
# Cell PMs: task management + sessions (no group creation)
|
||||||
_register_pm_completion_tools(mcp, client, agent_id)
|
_register_pm_completion_tools(mcp, client, agent_id)
|
||||||
_register_pm_tools(mcp, client, agent_id)
|
_register_pm_tools(mcp, client, agent_id)
|
||||||
_register_session_tools(mcp, client, agent_id)
|
_register_session_tools(mcp, client, agent_id)
|
||||||
_register_blocking_tools(mcp, client, agent_id)
|
_register_blocking_tools(mcp, client, agent_id)
|
||||||
|
|
||||||
elif role in ("product_owner", "head_marketing", "auditor", "ceo"):
|
elif role == "main_pm":
|
||||||
# Board/Management: PM tools + completion
|
# Main PM: full management including group creation
|
||||||
_register_pm_completion_tools(mcp, client, agent_id)
|
_register_pm_completion_tools(mcp, client, agent_id)
|
||||||
_register_pm_tools(mcp, client, agent_id)
|
_register_pm_tools(mcp, client, agent_id)
|
||||||
_register_session_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.)
|
# 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 []
|
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(
|
def get_scan_guidance(
|
||||||
paused_tasks: list[dict],
|
paused_tasks: list[dict],
|
||||||
assigned_tasks: list[dict],
|
assigned_tasks: list[dict],
|
||||||
@@ -51,6 +77,13 @@ def get_scan_guidance(
|
|||||||
f"You have {len(paused_tasks)} paused task(s). "
|
f"You have {len(paused_tasks)} paused task(s). "
|
||||||
"Resume your paused work before claiming new tasks."
|
"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:
|
if assigned_tasks:
|
||||||
return (
|
return (
|
||||||
f"You have {len(assigned_tasks)} active task(s). "
|
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:
|
def check_blocking_tasks(active_tasks: list[dict]) -> dict[str, Any] | None:
|
||||||
"""Check for blocking active tasks. Returns error or 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]
|
blocking = [t for t in active_tasks if t.get("status") in blocking_statuses]
|
||||||
if blocking:
|
if blocking:
|
||||||
|
status = blocking[0].get("status", "active")
|
||||||
return format_error_response(
|
return format_error_response(
|
||||||
"ALREADY_ACTIVE",
|
"ALREADY_ACTIVE",
|
||||||
f"You already have an active task: {blocking[0]['id']}. "
|
f"You have a {status} task: {blocking[0]['id']}. "
|
||||||
"Complete or pause it before claiming a new task.",
|
"Work on it first, or pause it if blocked.",
|
||||||
{"active_task_id": blocking[0]["id"]},
|
{"active_task_id": blocking[0]["id"], "status": status},
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -91,8 +125,14 @@ def check_paused_tasks(active_tasks: list[dict]) -> dict[str, Any] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def validate_task_claimable(task: dict, agent_role: str) -> dict[str, Any] | None:
|
async def validate_task_claimable(
|
||||||
"""Validate task can be claimed based on agent role."""
|
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")
|
task_status = task.get("status")
|
||||||
claimable_statuses = {
|
claimable_statuses = {
|
||||||
"qa": ["awaiting_qa"],
|
"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"])
|
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:
|
if task_status not in allowed:
|
||||||
return format_error_response(
|
return format_error_response(
|
||||||
"INVALID_STATE",
|
"INVALID_STATE",
|
||||||
@@ -148,6 +197,24 @@ def validate_task_status(
|
|||||||
return None
|
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(
|
async def validate_task_ownership(
|
||||||
task: dict, agent_id: str, client: ApiClient
|
task: dict, agent_id: str, client: ApiClient
|
||||||
) -> dict[str, Any] | None:
|
) -> 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]:
|
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 {
|
return {
|
||||||
"approach": plan_params["approach"],
|
"approach": plan_params["approach"],
|
||||||
"sub_tasks": [
|
"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"])
|
for i, st in enumerate(plan_params["sub_tasks"])
|
||||||
],
|
],
|
||||||
"risks": [{"description": r} for r in (plan_params.get("risks") or [])],
|
"risks": [{"description": r} for r in (plan_params.get("risks") or [])],
|
||||||
"open_questions": [
|
"open_questions": open_questions,
|
||||||
{"question": q, "answered": False}
|
|
||||||
for q in (plan_params.get("open_questions") or [])
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from roboco.agents_config import get_agent_role
|
|||||||
from roboco.mcp.tasks import format_task_response
|
from roboco.mcp.tasks import format_task_response
|
||||||
from roboco.mcp.tasks.handlers._helpers import (
|
from roboco.mcp.tasks.handlers._helpers import (
|
||||||
check_blocking_tasks,
|
check_blocking_tasks,
|
||||||
check_paused_tasks,
|
|
||||||
fetch_task_or_error,
|
fetch_task_or_error,
|
||||||
get_project_context,
|
get_project_context,
|
||||||
validate_task_claimable,
|
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:
|
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")
|
active_resp = await client.get("/tasks/my")
|
||||||
if not active_resp.ok:
|
if not active_resp.ok:
|
||||||
return None
|
return None
|
||||||
active_tasks = active_resp.json()
|
active_tasks = active_resp.json()
|
||||||
if error := check_blocking_tasks(active_tasks):
|
# Only block on in_progress tasks, not paused ones
|
||||||
return error
|
return check_blocking_tasks(active_tasks)
|
||||||
return check_paused_tasks(active_tasks)
|
|
||||||
|
|
||||||
|
|
||||||
async def _execute_claim(
|
async def _execute_claim(
|
||||||
@@ -58,7 +61,7 @@ async def handle_task_claim(
|
|||||||
assert task is not None
|
assert task is not None
|
||||||
|
|
||||||
agent_role = get_agent_role(agent_id)
|
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
|
return error
|
||||||
|
|
||||||
claimed_task, error = await _execute_claim(client, task_id, agent_id)
|
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.agents_config import can_cancel_tasks, get_agent_role
|
||||||
from roboco.mcp.tasks import format_task_response
|
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
|
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:
|
def _validate_documenter_role(agent_id: str) -> dict[str, Any] | None:
|
||||||
"""Validate agent is a documenter. Returns error or None."""
|
"""Validate agent is a documenter. Returns error or None."""
|
||||||
@@ -50,8 +56,8 @@ async def handle_docs_complete(
|
|||||||
return error
|
return error
|
||||||
assert task is not None
|
assert task is not None
|
||||||
|
|
||||||
if error := validate_task_status(
|
if error := validate_task_status_in(
|
||||||
task, "awaiting_documentation", "mark as docs complete"
|
task, DOCUMENTER_WORKFLOW_STATUSES, "mark as docs complete"
|
||||||
):
|
):
|
||||||
return error
|
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(
|
async def handle_submit_pm_review(
|
||||||
client: ApiClient, task_id: str, agent_id: str, notes: str | None = None
|
client: ApiClient, task_id: str, agent_id: str, notes: str | None = None
|
||||||
) -> dict[str, Any]:
|
) -> 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,
|
For tasks that don't follow the standard dev→QA→docs workflow,
|
||||||
such as PM validation tasks, QA audit tasks, or directly-assigned work.
|
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)
|
task, error = await fetch_task_or_error(client, task_id)
|
||||||
if error:
|
if error:
|
||||||
return error
|
return error
|
||||||
assert task is not None
|
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
|
# Must be in_progress to submit for PM review
|
||||||
current_status = task.get("status")
|
current_status = task.get("status")
|
||||||
if current_status != "in_progress":
|
if current_status != "in_progress":
|
||||||
|
|||||||
@@ -7,15 +7,20 @@ Handlers for task verification and QA review.
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from roboco.agents_config import get_agent_role
|
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 import format_task_response
|
||||||
from roboco.mcp.tasks.handlers._helpers import (
|
from roboco.mcp.tasks.handlers._helpers import (
|
||||||
fetch_task_or_error,
|
fetch_task_or_error,
|
||||||
validate_task_ownership,
|
validate_task_ownership,
|
||||||
validate_task_status,
|
validate_task_status,
|
||||||
|
validate_task_status_in,
|
||||||
)
|
)
|
||||||
from roboco.mcp.utils import ApiClient, format_error_response, resolve_agent_uuid_cached
|
from roboco.mcp.utils import ApiClient, format_error_response, resolve_agent_uuid_cached
|
||||||
from roboco.services.task import extract_original_developer
|
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:
|
def _validate_developer_role(agent_id: str) -> dict[str, Any] | None:
|
||||||
"""Validate agent is a developer (not PM/QA/Documenter). Returns error or 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")
|
quick_context = task.get("quick_context")
|
||||||
original_dev = extract_original_developer(quick_context)
|
original_dev = extract_original_developer(quick_context)
|
||||||
agent_uuid = await resolve_agent_uuid_cached(agent_id, client)
|
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 format_error_response("SELF_REVIEW", "Cannot review your own work.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -211,7 +216,7 @@ async def handle_task_qa_pass(
|
|||||||
return error
|
return error
|
||||||
assert task is not None
|
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
|
return error
|
||||||
|
|
||||||
if error := await _check_self_review(task, agent_id, client):
|
if error := await _check_self_review(task, agent_id, client):
|
||||||
@@ -262,7 +267,7 @@ async def handle_task_qa_fail(
|
|||||||
return error
|
return error
|
||||||
assert task is not None
|
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
|
return error
|
||||||
|
|
||||||
full_notes = f"{qa_notes}\n\nIssues:\n" + "\n".join(f"- {i}" for i in issues)
|
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):
|
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"
|
PENDING = "pending"
|
||||||
CLAIMED = "claimed"
|
CLAIMED = "claimed"
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ Handoff Model
|
|||||||
|
|
||||||
Documenter handoffs contain all the information needed for
|
Documenter handoffs contain all the information needed for
|
||||||
a Documenter to create production documentation from developer work.
|
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
|
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.orchestrator import AgentInstance, AgentOrchestrator, AgentState
|
||||||
|
from roboco.runtime.streaming import (
|
||||||
|
ReasoningStreamCallback,
|
||||||
|
get_reasoning_stream_callback,
|
||||||
|
set_reasoning_stream_callback,
|
||||||
|
stream_reasoning,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AgentInstance",
|
"AgentInstance",
|
||||||
"AgentOrchestrator",
|
"AgentOrchestrator",
|
||||||
"AgentState",
|
"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)
|
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]] = {
|
_AGENT_TEAM_MAP: ClassVar[dict[str, str]] = {
|
||||||
"main-pm": "main_pm",
|
"main-pm": "main_pm",
|
||||||
"product-owner": "board",
|
"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:
|
def _has_board_keywords(self, text: str) -> bool:
|
||||||
"""Check if text contains board-level keywords."""
|
"""Check if text contains board-level keywords."""
|
||||||
return any(kw in text for kw in self._BOARD_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."""
|
"""Check if text contains PM coordination keywords."""
|
||||||
return any(kw in text for kw in self._PM_KEYWORDS)
|
return any(kw in text for kw in self._PM_KEYWORDS)
|
||||||
|
|
||||||
|
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)
|
# Direct team-to-routing mappings (explicit assignments bypass keyword analysis)
|
||||||
_TEAM_ROUTING_MAP: ClassVar[dict[str, str]] = {
|
_TEAM_ROUTING_MAP: ClassVar[dict[str, str]] = {
|
||||||
"main_pm": "main_pm",
|
"main_pm": "main_pm",
|
||||||
@@ -1192,6 +1216,10 @@ Start by:
|
|||||||
if self._has_board_keywords(text):
|
if self._has_board_keywords(text):
|
||||||
return "board"
|
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
|
# High complexity or cross-team → Main PM
|
||||||
if complexity in ("high", "critical") or not team or team == "all":
|
if complexity in ("high", "critical") or not team or team == "all":
|
||||||
return "main_pm"
|
return "main_pm"
|
||||||
@@ -1242,8 +1270,87 @@ Start by:
|
|||||||
|
|
||||||
return None
|
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:
|
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")
|
task_id = task.get("id", "unknown")
|
||||||
title = task.get("title", "Untitled")
|
title = task.get("title", "Untitled")
|
||||||
complexity = task.get("complexity", "medium")
|
complexity = task.get("complexity", "medium")
|
||||||
@@ -1427,14 +1534,13 @@ Start now: roboco_task_get("{task_id}")
|
|||||||
tasks = await self._fetch_tasks(client, "pending")
|
tasks = await self._fetch_tasks(client, "pending")
|
||||||
|
|
||||||
# PM-level agents that can have direct assignments
|
# 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 = {
|
pm_agents = {
|
||||||
"main-pm",
|
"main-pm",
|
||||||
"be-pm",
|
"be-pm",
|
||||||
"fe-pm",
|
"fe-pm",
|
||||||
"ux-pm",
|
"ux-pm",
|
||||||
"product-owner",
|
|
||||||
"head-marketing",
|
|
||||||
"auditor",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
@@ -1449,10 +1555,16 @@ Start now: roboco_task_get("{task_id}")
|
|||||||
task_id=task.get("id"),
|
task_id=task.get("id"),
|
||||||
agent_id=agent_slug,
|
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(
|
await self.spawn_agent(
|
||||||
agent_id=agent_slug,
|
agent_id=agent_slug,
|
||||||
task_id=task["id"],
|
task_id=task["id"],
|
||||||
initial_prompt=self._build_pm_triage_prompt(task),
|
initial_prompt=pm_prompt,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -1482,9 +1594,11 @@ Start now: roboco_task_get("{task_id}")
|
|||||||
|
|
||||||
# Claim and spawn with appropriate prompt
|
# Claim and spawn with appropriate prompt
|
||||||
if await self._claim_task_for_agent(client, task["id"], agent_id):
|
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":
|
if routing == "dev":
|
||||||
prompt = self._build_dev_prompt(task)
|
prompt = self._build_dev_prompt(task)
|
||||||
|
elif routing == "main_pm" or agent_id == "main-pm":
|
||||||
|
prompt = self._build_main_pm_triage_prompt(task)
|
||||||
else:
|
else:
|
||||||
prompt = self._build_pm_triage_prompt(task)
|
prompt = self._build_pm_triage_prompt(task)
|
||||||
|
|
||||||
@@ -1624,18 +1738,31 @@ ALL SUBTASKS COMPLETED:
|
|||||||
Begin with step 1: roboco_task_get("{task_id}")
|
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:
|
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
|
needs_revision tasks. New unassigned pending tasks are handled by
|
||||||
_dispatch_pm_work() which routes them through the PM hierarchy.
|
_dispatch_pm_work() which routes them through the PM hierarchy.
|
||||||
|
|
||||||
Monitors: assigned pending tasks, needs_revision tasks
|
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"])
|
tasks = await self._fetch_tasks(client, ["pending", "needs_revision"])
|
||||||
|
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
@@ -1658,12 +1785,12 @@ Begin with step 1: roboco_task_get("{task_id}")
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# For pending tasks that ARE already assigned (by PM),
|
# 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):
|
if agent_slug and not self._is_agent_active(agent_slug):
|
||||||
await self.spawn_agent(
|
await self.spawn_agent(
|
||||||
agent_id=agent_slug,
|
agent_id=agent_slug,
|
||||||
task_id=task["id"],
|
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:
|
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"]:
|
if team not in ["backend", "frontend", "ux_ui"]:
|
||||||
continue
|
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")
|
agent_id = self._select_agent_for_cell(team, "qa")
|
||||||
if not agent_id:
|
if not agent_id:
|
||||||
continue
|
continue
|
||||||
@@ -1688,6 +1832,15 @@ Begin with step 1: roboco_task_get("{task_id}")
|
|||||||
# QA already running, they'll pick up on scan
|
# QA already running, they'll pick up on scan
|
||||||
continue
|
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
|
# Spawn QA agent with task assignment
|
||||||
await self.spawn_agent(
|
await self.spawn_agent(
|
||||||
agent_id=agent_id,
|
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"]:
|
if team not in ["backend", "frontend", "ux_ui"]:
|
||||||
continue
|
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")
|
agent_id = self._select_agent_for_cell(team, "doc")
|
||||||
if not agent_id:
|
if not agent_id:
|
||||||
continue
|
continue
|
||||||
@@ -1718,6 +1887,15 @@ Begin with step 1: roboco_task_get("{task_id}")
|
|||||||
if self._is_agent_active(agent_id):
|
if self._is_agent_active(agent_id):
|
||||||
continue
|
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(
|
await self.spawn_agent(
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
task_id=task["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"]:
|
if team not in ["backend", "frontend", "ux_ui"]:
|
||||||
continue
|
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")
|
pm_id = self._TEAM_PM_MAP.get(team, "be-pm")
|
||||||
|
|
||||||
if self._is_agent_active(pm_id):
|
if self._is_agent_active(pm_id):
|
||||||
continue
|
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(
|
await self.spawn_agent(
|
||||||
agent_id=pm_id,
|
agent_id=pm_id,
|
||||||
task_id=task["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
|
# 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"]
|
CEO_AGENT_ID = AGENT_UUIDS["ceo"]
|
||||||
@@ -405,7 +419,7 @@ Check `roboco_task_scan(team="frontend")` for pending frontend tasks.
|
|||||||
- Prototyping
|
- Prototyping
|
||||||
- Accessibility
|
- 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
|
# "cell" scope means can only notify own cell members
|
||||||
if scope == "cell":
|
if scope == "cell":
|
||||||
# Cell PM can only notify their own cell unless coordinating with PMs
|
# Cell PM can notify other PMs (Cell PMs or Main PM) for coordination
|
||||||
if recipient.role == AgentRole.CELL_PM:
|
if recipient.role in (AgentRole.CELL_PM, AgentRole.MAIN_PM):
|
||||||
# PMs can notify other PMs for coordination
|
|
||||||
return True
|
return True
|
||||||
# Otherwise must be same team
|
# Otherwise must be same team
|
||||||
return sender.team == recipient.team
|
return sender.team == recipient.team
|
||||||
|
|||||||
+108
-30
@@ -16,6 +16,7 @@ from roboco.db.tables import AgentTable, SessionTaskTable, TaskTable
|
|||||||
from roboco.enforcement import (
|
from roboco.enforcement import (
|
||||||
TaskOwnershipError,
|
TaskOwnershipError,
|
||||||
validate_task_ownership,
|
validate_task_ownership,
|
||||||
|
validate_task_transition,
|
||||||
)
|
)
|
||||||
from roboco.models.base import TaskStatus, Team
|
from roboco.models.base import TaskStatus, Team
|
||||||
from roboco.models.task import TaskCreateRequest
|
from roboco.models.task import TaskCreateRequest
|
||||||
@@ -57,7 +58,13 @@ def _get_valid_claim_statuses(
|
|||||||
if role == "qa":
|
if role == "qa":
|
||||||
return {TaskStatus.AWAITING_QA}
|
return {TaskStatus.AWAITING_QA}
|
||||||
elif role == "documenter":
|
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:
|
else:
|
||||||
# Developer, PM, and other roles
|
# Developer, PM, and other roles
|
||||||
statuses = {TaskStatus.PENDING}
|
statuses = {TaskStatus.PENDING}
|
||||||
@@ -110,6 +117,48 @@ class TaskService(BaseService):
|
|||||||
|
|
||||||
service_name: ClassVar[str] = "task"
|
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
|
# CRUD OPERATIONS
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -381,7 +430,7 @@ class TaskService(BaseService):
|
|||||||
Role-based claiming:
|
Role-based claiming:
|
||||||
- Developers/PMs: can claim PENDING tasks
|
- Developers/PMs: can claim PENDING tasks
|
||||||
- QA: can claim AWAITING_QA 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)
|
task = await self.get(task_id)
|
||||||
if not task:
|
if not task:
|
||||||
@@ -411,18 +460,17 @@ class TaskService(BaseService):
|
|||||||
task.assigned_to = cast("Any", agent_id)
|
task.assigned_to = cast("Any", agent_id)
|
||||||
task.claimed_at = datetime.now(UTC)
|
task.claimed_at = datetime.now(UTC)
|
||||||
|
|
||||||
# Transition to CLAIMED from any claimable status
|
# Transition to CLAIMED - validated with role for proper enforcement
|
||||||
# (PENDING, AWAITING_QA, AWAITING_DOCUMENTATION all → CLAIMED)
|
agent_role = agent.role.value if agent and agent.role else None
|
||||||
claimable_statuses = {
|
claimable_statuses = {
|
||||||
TaskStatus.PENDING,
|
TaskStatus.PENDING,
|
||||||
TaskStatus.AWAITING_QA,
|
TaskStatus.AWAITING_QA,
|
||||||
TaskStatus.AWAITING_DOCUMENTATION,
|
TaskStatus.AWAITING_DOCUMENTATION,
|
||||||
}
|
}
|
||||||
if task.status in claimable_statuses:
|
if task.status in claimable_statuses:
|
||||||
task.status = TaskStatus.CLAIMED
|
self._validate_and_set_status(task, TaskStatus.CLAIMED, agent_role)
|
||||||
|
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
self.log.info("Task claimed", task_id=str(task_id), agent_id=str(agent_id))
|
|
||||||
return task
|
return task
|
||||||
|
|
||||||
async def start(
|
async def start(
|
||||||
@@ -490,10 +538,8 @@ class TaskService(BaseService):
|
|||||||
# Only update started_at if this is the first time starting
|
# Only update started_at if this is the first time starting
|
||||||
if task.started_at is None:
|
if task.started_at is None:
|
||||||
task.started_at = datetime.now(UTC)
|
task.started_at = datetime.now(UTC)
|
||||||
task.status = TaskStatus.IN_PROGRESS
|
self._validate_and_set_status(task, TaskStatus.IN_PROGRESS)
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
|
|
||||||
self.log.info("Task started", task_id=str(task_id))
|
|
||||||
return task
|
return task
|
||||||
|
|
||||||
async def block(self, task_id: UUID, blocker_task_id: UUID) -> TaskTable | None:
|
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:
|
if blocker_task_id not in task.dependency_ids:
|
||||||
new_deps = [*task.dependency_ids, blocker_task_id]
|
new_deps = [*task.dependency_ids, blocker_task_id]
|
||||||
task.dependency_ids = new_deps
|
task.dependency_ids = new_deps
|
||||||
task.status = TaskStatus.BLOCKED
|
self._validate_and_set_status(task, TaskStatus.BLOCKED)
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
|
|
||||||
# Update the blocker task to reference this as blocked
|
# Update the blocker task to reference this as blocked
|
||||||
@@ -645,13 +691,15 @@ class TaskService(BaseService):
|
|||||||
if task.status != TaskStatus.VERIFYING:
|
if task.status != TaskStatus.VERIFYING:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Store original developer BEFORE QA claims - this is the authoritative record
|
# Store original developer BEFORE clearing assignment - authoritative
|
||||||
# for self-review prevention. Storing here ensures we capture the developer
|
# record for self-review prevention (QA can't review own work).
|
||||||
# even if the task is reassigned before QA claims it.
|
|
||||||
original_dev = str(task.assigned_to) if task.assigned_to else None
|
original_dev = str(task.assigned_to) if task.assigned_to else None
|
||||||
if original_dev:
|
if original_dev:
|
||||||
task.quick_context = f"original_developer:{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.self_verified = True
|
||||||
task.status = TaskStatus.AWAITING_QA
|
task.status = TaskStatus.AWAITING_QA
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
@@ -666,16 +714,29 @@ class TaskService(BaseService):
|
|||||||
async def pass_qa(
|
async def pass_qa(
|
||||||
self, task_id: UUID, notes: str | None = None
|
self, task_id: UUID, notes: str | None = None
|
||||||
) -> TaskTable | 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)
|
task = await self.get(task_id)
|
||||||
if not task:
|
if not task:
|
||||||
return None
|
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
|
return None
|
||||||
|
|
||||||
if notes:
|
if notes:
|
||||||
task.qa_notes = notes
|
task.qa_notes = notes
|
||||||
|
# Clear assignment so documenter can claim the task
|
||||||
|
task.assigned_to = None
|
||||||
task.qa_verified = True
|
task.qa_verified = True
|
||||||
task.status = TaskStatus.AWAITING_DOCUMENTATION
|
task.status = TaskStatus.AWAITING_DOCUMENTATION
|
||||||
await self.session.flush()
|
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.
|
When QA fails a task, it goes back to the original developer for revision.
|
||||||
The original developer is extracted from quick_context which stores
|
The original developer is extracted from quick_context which stores
|
||||||
"original_developer:{uuid}" when the task was submitted to QA.
|
"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)
|
task = await self.get(task_id)
|
||||||
if not task:
|
if not task:
|
||||||
return None
|
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
|
return None
|
||||||
|
|
||||||
task.qa_notes = notes
|
task.qa_notes = notes
|
||||||
@@ -732,8 +802,8 @@ class TaskService(BaseService):
|
|||||||
"""
|
"""
|
||||||
Mark documentation as complete (documenter only).
|
Mark documentation as complete (documenter only).
|
||||||
|
|
||||||
Transitions task from AWAITING_DOCUMENTATION to AWAITING_PM_REVIEW.
|
Documenter workflow: awaiting_documentation → claim → plan → start
|
||||||
The Cell PM will then review and call complete() to finish the task.
|
→ docs_complete. Accept claimed/in_progress (documenter working).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task_id: The task to mark docs complete
|
task_id: The task to mark docs complete
|
||||||
@@ -746,9 +816,16 @@ class TaskService(BaseService):
|
|||||||
if not task:
|
if not task:
|
||||||
return None
|
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(
|
self.log.warning(
|
||||||
"Cannot mark docs complete - not awaiting documentation",
|
"Cannot mark docs complete - invalid status for documenter workflow",
|
||||||
task_id=str(task_id),
|
task_id=str(task_id),
|
||||||
current_status=task.status.value,
|
current_status=task.status.value,
|
||||||
)
|
)
|
||||||
@@ -778,8 +855,9 @@ class TaskService(BaseService):
|
|||||||
else doc_context
|
else doc_context
|
||||||
)
|
)
|
||||||
|
|
||||||
# Note: We don't auto-assign to PM here - PM will pick it up via scan
|
# Clear assignment so PM can claim the task for review
|
||||||
# The task remains assigned to documenter until PM claims it
|
# Documenter info is preserved in quick_context
|
||||||
|
task.assigned_to = None
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
|
|
||||||
self.log.info(
|
self.log.info(
|
||||||
@@ -884,25 +962,25 @@ class TaskService(BaseService):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
task.completed_at = datetime.now(UTC)
|
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()
|
await self.session.flush()
|
||||||
|
|
||||||
# Unblock any tasks waiting on this one
|
# Unblock any tasks waiting on this one
|
||||||
await self._unblock_dependents(task_id)
|
await self._unblock_dependents(task_id)
|
||||||
|
|
||||||
self.log.info("Task completed", task_id=str(task_id))
|
|
||||||
return task
|
return task
|
||||||
|
|
||||||
async def cancel(self, task_id: UUID) -> TaskTable | None:
|
async def cancel(
|
||||||
"""Cancel a task."""
|
self, task_id: UUID, agent_role: str = "cell_pm"
|
||||||
|
) -> TaskTable | None:
|
||||||
|
"""Cancel a task (PM only)."""
|
||||||
task = await self.get(task_id)
|
task = await self.get(task_id)
|
||||||
if not task:
|
if not task:
|
||||||
return None
|
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()
|
await self.session.flush()
|
||||||
|
|
||||||
self.log.info("Task cancelled", task_id=str(task_id))
|
|
||||||
return task
|
return task
|
||||||
|
|
||||||
async def _unblock_dependents(self, completed_task_id: UUID) -> None:
|
async def _unblock_dependents(self, completed_task_id: UUID) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user