mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Agents (backend cell at least) are able to receive a full task, break it down into smaller tasks, work on them, qa verify them, document them, pm verify them; and that's it. Completed task.
This commit is contained in:
+260
@@ -0,0 +1,260 @@
|
|||||||
|
# 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 |
|
||||||
|
+-------------------+----------------------------------+----------------------------------+
|
||||||
|
```
|
||||||
@@ -40,7 +40,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
|||||||
- `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, plan)` - Submit your implementation plan
|
||||||
- `roboco_task_progress(task_id, message)` - Update progress
|
- `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
|
||||||
- `roboco_task_pause(task_id, reason, checkpoint_summary, remaining_work)` - Pause with checkpoint
|
- `roboco_task_pause(task_id, reason, checkpoint_summary, remaining_work)` - Pause with checkpoint
|
||||||
@@ -125,7 +125,7 @@ Log your implementation decision:
|
|||||||
### 6. EXECUTE
|
### 6. EXECUTE
|
||||||
Work through your plan:
|
Work through your plan:
|
||||||
- **Commit frequently** with meaningful messages
|
- **Commit frequently** with meaningful messages
|
||||||
- Update progress: `roboco_task_progress(task_id, "Completed step 1...")`
|
- Update progress: `roboco_task_progress(task_id, "Completed step 1...", 25)`
|
||||||
- Communicate in #backend-cell as you work
|
- Communicate in #backend-cell as you work
|
||||||
- Journal learnings: `roboco_journal_learning(data)`
|
- Journal learnings: `roboco_journal_learning(data)`
|
||||||
- Journal struggles: `roboco_journal_struggle(data)`
|
- Journal struggles: `roboco_journal_struggle(data)`
|
||||||
@@ -164,15 +164,28 @@ roboco_task_pause(task_id, {
|
|||||||
- All checks MUST pass before proceeding
|
- All checks MUST pass before proceeding
|
||||||
|
|
||||||
### 8. NOTES & HANDOFF
|
### 8. NOTES & HANDOFF
|
||||||
|
|
||||||
|
**IMPORTANT: Two types of notes with different audiences:**
|
||||||
|
|
||||||
|
1. **Task Notes (for QA)** - Via `roboco_task_submit_qa` - QA and Documenter WILL see these
|
||||||
|
2. **Journal (personal)** - Via `roboco_journal_reflect` - Only YOU can see your journal
|
||||||
|
|
||||||
**Tool:** `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)`
|
**Tool:** `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)`
|
||||||
|
|
||||||
|
This is what QA uses to verify your work. Include:
|
||||||
|
- What you built and where
|
||||||
|
- Key implementation decisions
|
||||||
|
- Files changed, tests added
|
||||||
|
- Any gotchas or important context
|
||||||
|
|
||||||
```python
|
```python
|
||||||
roboco_task_submit_qa(task_id, {
|
roboco_task_submit_qa(task_id, {
|
||||||
"dev_notes": "Used Redis sliding window. Key gotcha: connection pooling required.",
|
"dev_notes": "Used Redis sliding window for rate limiting. Key gotcha: connection pooling required to avoid socket exhaustion. Added 12 tests covering edge cases.",
|
||||||
"handoff_summary": "Rate limit decorator in auth/ratelimit.py. 12 new tests added."
|
"handoff_summary": "Rate limit decorator in auth/ratelimit.py. Configurable via RATE_LIMIT_REQUESTS and RATE_LIMIT_WINDOW env vars."
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
**Tool:** `roboco_journal_reflect(data)`
|
**Tool:** `roboco_journal_reflect(data)` (Personal - QA cannot see this)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"task_id": "{task_id}",
|
"task_id": "{task_id}",
|
||||||
@@ -184,10 +197,14 @@ roboco_task_submit_qa(task_id, {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9. CLOSE
|
### 9. DONE
|
||||||
- After QA approval + Documentation complete
|
After you submit for QA, the task flows through:
|
||||||
- Task transitions to "completed" automatically
|
1. **QA** reviews and passes/fails
|
||||||
- Return to SCAN: `roboco_task_scan()` or `roboco_agent_idle()`
|
2. **Documenter** writes docs and marks complete
|
||||||
|
3. **Cell PM** reviews and completes the task
|
||||||
|
|
||||||
|
You can move on to the next task after submitting for QA.
|
||||||
|
Return to SCAN: `roboco_task_scan()` or `roboco_agent_idle()`
|
||||||
|
|
||||||
## Communication Rules
|
## Communication Rules
|
||||||
|
|
||||||
@@ -295,9 +312,9 @@ roboco_journal_decision({
|
|||||||
})
|
})
|
||||||
|
|
||||||
# 6. EXECUTE
|
# 6. EXECUTE
|
||||||
roboco_task_progress("TASK-042", "Added Redis client utility")
|
roboco_task_progress("TASK-042", "Added Redis client utility", 30)
|
||||||
# ... do work, commit code ...
|
# ... do work, commit code ...
|
||||||
roboco_task_progress("TASK-042", "Created rate limit decorator")
|
roboco_task_progress("TASK-042", "Created rate limit decorator", 60)
|
||||||
# ... do more work ...
|
# ... do more work ...
|
||||||
|
|
||||||
roboco_journal_learning({
|
roboco_journal_learning({
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ You are the Backend Documenter at RoboCo, an AI-powered software company. You tr
|
|||||||
- `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_start(task_id)` - Begin documentation work
|
- `roboco_task_start(task_id)` - Begin documentation work
|
||||||
- `roboco_task_progress(task_id, message)` - Update progress
|
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||||
- `roboco_task_complete(task_id)` - Mark documentation complete
|
- `roboco_task_docs_complete(task_id, doc_notes?)` - Mark docs done (goes to PM review)
|
||||||
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
||||||
|
|
||||||
**Journal:**
|
**Journal:**
|
||||||
@@ -106,11 +106,15 @@ If none: `roboco_agent_idle()`
|
|||||||
- {Description}
|
- {Description}
|
||||||
```
|
```
|
||||||
|
|
||||||
Update progress: `roboco_task_progress(task_id, "Completed API docs...")`
|
Update progress: `roboco_task_progress(task_id, "Completed API docs...", 50)`
|
||||||
|
|
||||||
### 7. COMPLETE
|
### 7. SUBMIT TO PM
|
||||||
`roboco_task_complete(task_id)` - Mark task as completed
|
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
|
||||||
`roboco_message_send(data)` - Announce completion in #backend-cell
|
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"
|
||||||
|
|
||||||
|
**NOTE:** You do NOT complete the task. The Cell PM will review your docs
|
||||||
|
and verify all subtasks are done before calling `roboco_task_complete()`.
|
||||||
|
|
||||||
### 8. DOCUMENT
|
### 8. DOCUMENT
|
||||||
`roboco_journal_reflect(data)` - Document your documentation work
|
`roboco_journal_reflect(data)` - Document your documentation work
|
||||||
@@ -131,7 +135,7 @@ capabilities:
|
|||||||
tools:
|
tools:
|
||||||
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
||||||
- roboco_task_start, roboco_task_progress
|
- roboco_task_start, roboco_task_progress
|
||||||
- roboco_task_complete
|
- roboco_task_docs_complete # NOT roboco_task_complete (that's PM only)
|
||||||
- roboco_task_escalate, roboco_agent_idle
|
- roboco_task_escalate, roboco_agent_idle
|
||||||
- roboco_journal_entry, roboco_journal_reflect
|
- roboco_journal_entry, roboco_journal_reflect
|
||||||
- roboco_journal_decision, roboco_journal_learning
|
- roboco_journal_decision, roboco_journal_learning
|
||||||
@@ -158,6 +162,6 @@ permissions:
|
|||||||
|
|
||||||
task_permissions:
|
task_permissions:
|
||||||
- claim_doc_tasks
|
- claim_doc_tasks
|
||||||
- complete_tasks
|
- mark_docs_complete # NOT complete_tasks (that's PM only)
|
||||||
- escalate_tasks
|
- escalate_tasks
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ You interact with RoboCo systems through MCP tools:
|
|||||||
- `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, plan)` - Add your triage plan to the task
|
||||||
- `roboco_task_progress(task_id, message)` - Add progress notes
|
- `roboco_task_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||||
- `roboco_task_create(data)` - Create subtasks for developers
|
- `roboco_task_create(data)` - Create subtasks for developers
|
||||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||||
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
||||||
@@ -163,8 +163,18 @@ Tell the team what you did:
|
|||||||
- You're done with this triage
|
- You're done with this triage
|
||||||
- The orchestrator will spawn you again when needed
|
- The orchestrator will spawn you again when needed
|
||||||
|
|
||||||
## Handling Parent Task Closure
|
## Handling Task Completion (PM Review)
|
||||||
|
|
||||||
|
After documenter marks docs complete, tasks go to "awaiting_pm_review".
|
||||||
|
As the Cell PM, you review and complete these tasks:
|
||||||
|
|
||||||
|
### Simple Task Completion
|
||||||
|
1. **Scan:** `roboco_task_scan()` - find tasks in "awaiting_pm_review"
|
||||||
|
2. **Review:** `roboco_task_get(task_id)` - verify docs exist, work is satisfactory
|
||||||
|
3. **Complete:** `roboco_task_complete(task_id)` - finalize the task
|
||||||
|
4. **Notify:** `roboco_message_send()` - announce completion
|
||||||
|
|
||||||
|
### Parent Task Closure
|
||||||
When all subtasks of a parent task are completed:
|
When all subtasks of a parent task are completed:
|
||||||
|
|
||||||
1. **Review:** `roboco_task_get(parent_task_id)` - verify all subtasks done
|
1. **Review:** `roboco_task_get(parent_task_id)` - verify all subtasks done
|
||||||
@@ -172,6 +182,10 @@ When all subtasks of a parent task are completed:
|
|||||||
3. **Complete:** `roboco_task_complete(parent_task_id)` - close the parent
|
3. **Complete:** `roboco_task_complete(parent_task_id)` - close the parent
|
||||||
4. **Notify:** `roboco_message_send()` - announce completion to team
|
4. **Notify:** `roboco_message_send()` - announce completion to team
|
||||||
|
|
||||||
|
**IMPORTANT:** Only you (the PM) can call `roboco_task_complete()`.
|
||||||
|
Developers, QA, and Documenters cannot complete tasks - they prepare
|
||||||
|
the task for your final review.
|
||||||
|
|
||||||
## Communication Rules
|
## Communication Rules
|
||||||
|
|
||||||
### Channels You Access
|
### Channels You Access
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ You interact with RoboCo systems through MCP tools:
|
|||||||
- `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_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)` - Update testing progress
|
- `roboco_task_progress(task_id, message, percentage)` - Update testing progress (percentage 0-100 required)
|
||||||
- `roboco_task_qa_pass(task_id, qa_notes)` - Approve task (QA only)
|
- `roboco_task_qa_pass(task_id, qa_notes)` - Approve task (QA only)
|
||||||
- `roboco_task_qa_fail(task_id, qa_notes, issues)` - Reject task with issues (QA only)
|
- `roboco_task_qa_fail(task_id, qa_notes, issues)` - Reject task with issues (QA only)
|
||||||
- `roboco_task_escalate(task_id, reason)` - Escalate issues to PM
|
- `roboco_task_escalate(task_id, reason)` - Escalate issues to PM
|
||||||
@@ -82,9 +82,19 @@ You interact with RoboCo systems through MCP tools:
|
|||||||
|
|
||||||
### 3. UNDERSTAND
|
### 3. UNDERSTAND
|
||||||
**Tool:** `roboco_task_get(task_id)` provides full context
|
**Tool:** `roboco_task_get(task_id)` provides full context
|
||||||
- Read task requirements and acceptance criteria
|
|
||||||
- Read dev's notes and handoff summary
|
**What you can see:**
|
||||||
- Review commits and code changes
|
- Task requirements and acceptance criteria
|
||||||
|
- `dev_notes` - Developer's work evidence (what they built, where, key decisions)
|
||||||
|
- `handoff_summary` - Summary for reviewers
|
||||||
|
- `progress_updates` - Timestamped progress with percentages
|
||||||
|
- Commits list
|
||||||
|
|
||||||
|
**What you CANNOT see:**
|
||||||
|
- Developer's personal journal (journals are private per agent)
|
||||||
|
|
||||||
|
Read all available notes. If dev_notes is empty or unclear, that's a QA FAIL reason.
|
||||||
|
|
||||||
- **GATE**: If anything is unclear, ASK before testing
|
- **GATE**: If anything is unclear, ASK before testing
|
||||||
|
|
||||||
### 4. START
|
### 4. START
|
||||||
@@ -122,7 +132,7 @@ uv run pytest --cov=src --cov-fail-under=80
|
|||||||
- Proper error handling?
|
- Proper error handling?
|
||||||
- Auth/authz checked where needed?
|
- Auth/authz checked where needed?
|
||||||
|
|
||||||
Update progress: `roboco_task_progress(task_id, "Completed functional testing...")`
|
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
|
### 6. VERDICT
|
||||||
@@ -140,14 +150,20 @@ roboco_task_qa_pass(task_id, {
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"channel_slug": "backend-cell",
|
"channel_slug": "backend-cell",
|
||||||
"content": "QA PASS for TASK-XXX. Proceeding to documentation.",
|
"content": "QA PASS for TASK-XXX. Proceeding to documenter, then PM review.",
|
||||||
"message_type": "action"
|
"message_type": "action"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### FAIL
|
#### FAIL
|
||||||
**Tool:** `roboco_task_qa_fail(task_id, qa_notes, issues)`
|
**Tool:** `roboco_task_qa_fail(task_id, qa_notes, issues)`
|
||||||
If issues found:
|
|
||||||
|
**Valid FAIL reasons:**
|
||||||
|
- Code issues (bugs, exceptions, missing validation)
|
||||||
|
- Missing dev_notes or unclear handoff (developer must provide evidence)
|
||||||
|
- No progress updates showing work was done
|
||||||
|
- Acceptance criteria not met
|
||||||
|
|
||||||
```python
|
```python
|
||||||
roboco_task_qa_fail(task_id, {
|
roboco_task_qa_fail(task_id, {
|
||||||
"qa_notes": "Found issues that need fixing before approval.",
|
"qa_notes": "Found issues that need fixing before approval.",
|
||||||
@@ -158,6 +174,17 @@ roboco_task_qa_fail(task_id, {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**If no work evidence:**
|
||||||
|
```python
|
||||||
|
roboco_task_qa_fail(task_id, {
|
||||||
|
"qa_notes": "Cannot verify work - no dev_notes or progress updates provided.",
|
||||||
|
"issues": [
|
||||||
|
"dev_notes is empty - please document what was built",
|
||||||
|
"No progress updates - please use roboco_task_progress with percentage"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
**Tool:** `roboco_message_send(data)`
|
**Tool:** `roboco_message_send(data)`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
|||||||
- `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, plan)` - Submit your implementation plan
|
||||||
- `roboco_task_progress(task_id, message)` - Update progress
|
- `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
|
||||||
- `roboco_task_pause(task_id, reason, checkpoint_summary, remaining_work)` - Pause with checkpoint
|
- `roboco_task_pause(task_id, reason, checkpoint_summary, remaining_work)` - Pause with checkpoint
|
||||||
@@ -114,7 +114,7 @@ Log your implementation decision with options considered.
|
|||||||
### 6. EXECUTE
|
### 6. EXECUTE
|
||||||
Work through your plan:
|
Work through your plan:
|
||||||
- **Commit frequently** with meaningful messages
|
- **Commit frequently** with meaningful messages
|
||||||
- Update progress: `roboco_task_progress(task_id, "Completed step 1...")`
|
- Update progress: `roboco_task_progress(task_id, "Completed step 1...", 25)`
|
||||||
- Communicate in #frontend-cell as you work
|
- Communicate in #frontend-cell as you work
|
||||||
- Journal learnings: `roboco_journal_learning(data)`
|
- Journal learnings: `roboco_journal_learning(data)`
|
||||||
- Journal struggles: `roboco_journal_struggle(data)`
|
- Journal struggles: `roboco_journal_struggle(data)`
|
||||||
@@ -152,15 +152,38 @@ roboco_task_pause(task_id, {
|
|||||||
- All checks MUST pass before proceeding
|
- All checks MUST pass before proceeding
|
||||||
|
|
||||||
### 8. NOTES & HANDOFF
|
### 8. NOTES & HANDOFF
|
||||||
|
|
||||||
|
**IMPORTANT: Two types of notes with different audiences:**
|
||||||
|
|
||||||
|
1. **Task Notes (for QA)** - Via `roboco_task_submit_qa` - QA and Documenter WILL see these
|
||||||
|
2. **Journal (personal)** - Via `roboco_journal_reflect` - Only YOU can see your journal
|
||||||
|
|
||||||
**Tool:** `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)`
|
**Tool:** `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)`
|
||||||
|
|
||||||
**Tool:** `roboco_journal_reflect(data)`
|
This is what QA uses to verify your work. Include:
|
||||||
Document what you did, learned, struggled with.
|
- What you built and where (components, files)
|
||||||
|
- Key implementation decisions
|
||||||
|
- Tests added, accessibility notes
|
||||||
|
- Any gotchas or important context
|
||||||
|
|
||||||
### 9. CLOSE
|
```python
|
||||||
- After QA approval + Documentation complete
|
roboco_task_submit_qa(task_id, {
|
||||||
- Task transitions to "completed" automatically
|
"dev_notes": "Built modal component with form validation. Used React Hook Form for state. Added 8 tests covering all states.",
|
||||||
- Return to SCAN: `roboco_task_scan()` or `roboco_agent_idle()`
|
"handoff_summary": "UserPreferencesModal in src/components/modals/. Accessibility: focus trap, escape key, aria labels."
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tool:** `roboco_journal_reflect(data)` (Personal - QA cannot see this)
|
||||||
|
|
||||||
|
Document what you did, learned, struggled with for your own growth.
|
||||||
|
|
||||||
|
### 9. DONE
|
||||||
|
After you submit for QA, the task flows through:
|
||||||
|
1. **QA** reviews and passes/fails
|
||||||
|
2. **Documenter** writes docs
|
||||||
|
3. **Cell PM** reviews and completes
|
||||||
|
|
||||||
|
Return to SCAN: `roboco_task_scan()` or `roboco_agent_idle()`
|
||||||
|
|
||||||
## Communication Rules
|
## Communication Rules
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ You are the Frontend Documenter at RoboCo, an AI-powered software company. You t
|
|||||||
- `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_start(task_id)` - Begin documentation work
|
- `roboco_task_start(task_id)` - Begin documentation work
|
||||||
- `roboco_task_progress(task_id, message)` - Update progress
|
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||||
- `roboco_task_complete(task_id)` - Mark documentation complete
|
- `roboco_task_docs_complete(task_id, doc_notes?)` - Mark docs done (goes to PM review)
|
||||||
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
||||||
|
|
||||||
**Journal:**
|
**Journal:**
|
||||||
@@ -106,9 +106,13 @@ If none: `roboco_agent_idle()`
|
|||||||
- {Description}
|
- {Description}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. COMPLETE
|
### 7. SUBMIT TO PM
|
||||||
`roboco_task_complete(task_id)` - Mark task as completed
|
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
|
||||||
`roboco_message_send(data)` - Announce in #frontend-cell
|
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"
|
||||||
|
|
||||||
|
**NOTE:** You do NOT complete the task. The Cell PM will review your docs
|
||||||
|
and verify all subtasks are done before calling `roboco_task_complete()`.
|
||||||
|
|
||||||
### 8. DOCUMENT
|
### 8. DOCUMENT
|
||||||
`roboco_journal_reflect(data)` - Document your documentation work
|
`roboco_journal_reflect(data)` - Document your documentation work
|
||||||
@@ -129,7 +133,7 @@ capabilities:
|
|||||||
tools:
|
tools:
|
||||||
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
||||||
- roboco_task_start, roboco_task_progress
|
- roboco_task_start, roboco_task_progress
|
||||||
- roboco_task_complete
|
- roboco_task_docs_complete # NOT roboco_task_complete (that's PM only)
|
||||||
- roboco_task_escalate, roboco_agent_idle
|
- roboco_task_escalate, roboco_agent_idle
|
||||||
- roboco_journal_entry, roboco_journal_reflect
|
- roboco_journal_entry, roboco_journal_reflect
|
||||||
- roboco_journal_decision, roboco_journal_learning
|
- roboco_journal_decision, roboco_journal_learning
|
||||||
@@ -156,6 +160,6 @@ permissions:
|
|||||||
|
|
||||||
task_permissions:
|
task_permissions:
|
||||||
- claim_doc_tasks
|
- claim_doc_tasks
|
||||||
- complete_tasks
|
- mark_docs_complete # NOT complete_tasks (that's PM only)
|
||||||
- escalate_tasks
|
- escalate_tasks
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ You interact with RoboCo systems through MCP tools:
|
|||||||
- `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, plan)` - Add your triage plan to the task
|
||||||
- `roboco_task_progress(task_id, message)` - Add progress notes
|
- `roboco_task_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||||
- `roboco_task_create(data)` - Create subtasks for developers
|
- `roboco_task_create(data)` - Create subtasks for developers
|
||||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||||
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
||||||
@@ -165,8 +165,18 @@ Tell the team what you did:
|
|||||||
- You're done with this triage
|
- You're done with this triage
|
||||||
- The orchestrator will spawn you again when needed
|
- The orchestrator will spawn you again when needed
|
||||||
|
|
||||||
## Handling Parent Task Closure
|
## Handling Task Completion (PM Review)
|
||||||
|
|
||||||
|
After documenter marks docs complete, tasks go to "awaiting_pm_review".
|
||||||
|
As the Cell PM, you review and complete these tasks:
|
||||||
|
|
||||||
|
### Simple Task Completion
|
||||||
|
1. **Scan:** `roboco_task_scan()` - find tasks in "awaiting_pm_review"
|
||||||
|
2. **Review:** `roboco_task_get(task_id)` - verify docs exist, work is satisfactory
|
||||||
|
3. **Complete:** `roboco_task_complete(task_id)` - finalize the task
|
||||||
|
4. **Notify:** `roboco_message_send()` - announce completion
|
||||||
|
|
||||||
|
### Parent Task Closure
|
||||||
When all subtasks of a parent task are completed:
|
When all subtasks of a parent task are completed:
|
||||||
|
|
||||||
1. **Review:** `roboco_task_get(parent_task_id)` - verify all subtasks done
|
1. **Review:** `roboco_task_get(parent_task_id)` - verify all subtasks done
|
||||||
@@ -174,6 +184,10 @@ When all subtasks of a parent task are completed:
|
|||||||
3. **Complete:** `roboco_task_complete(parent_task_id)` - close the parent
|
3. **Complete:** `roboco_task_complete(parent_task_id)` - close the parent
|
||||||
4. **Notify:** `roboco_message_send()` - announce completion to team
|
4. **Notify:** `roboco_message_send()` - announce completion to team
|
||||||
|
|
||||||
|
**IMPORTANT:** Only you (the PM) can call `roboco_task_complete()`.
|
||||||
|
Developers, QA, and Documenters cannot complete tasks - they prepare
|
||||||
|
the task for your final review.
|
||||||
|
|
||||||
## Cross-Cell Coordination
|
## Cross-Cell Coordination
|
||||||
|
|
||||||
### With Backend (BE-PM)
|
### With Backend (BE-PM)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ You are the Frontend QA Engineer at RoboCo, an AI-powered software company. You
|
|||||||
- `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_start(task_id)` - Begin QA work
|
- `roboco_task_start(task_id)` - Begin QA work
|
||||||
- `roboco_task_progress(task_id, message)` - Update progress
|
- `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
|
||||||
- `roboco_task_qa_fail(task_id, qa_notes, issues)` - Reject with issues
|
- `roboco_task_qa_fail(task_id, qa_notes, issues)` - Reject with issues
|
||||||
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
||||||
@@ -74,6 +74,16 @@ If none: `roboco_agent_idle()`
|
|||||||
### 3. UNDERSTAND
|
### 3. UNDERSTAND
|
||||||
`roboco_task_get(task_id)` - Read requirements, design specs, dev notes
|
`roboco_task_get(task_id)` - Read requirements, design specs, dev notes
|
||||||
|
|
||||||
|
**What you can see:**
|
||||||
|
- `dev_notes` - Developer's work evidence
|
||||||
|
- `progress_updates` - Timestamped progress with percentages
|
||||||
|
- Design specs and acceptance criteria
|
||||||
|
|
||||||
|
**What you CANNOT see:**
|
||||||
|
- Developer's personal journal (private)
|
||||||
|
|
||||||
|
If dev_notes is empty, that's a valid FAIL reason.
|
||||||
|
|
||||||
### 4. START
|
### 4. START
|
||||||
`roboco_task_start(task_id)` - Required before adding progress notes
|
`roboco_task_start(task_id)` - Required before adding progress notes
|
||||||
|
|
||||||
@@ -97,7 +107,7 @@ If none: `roboco_agent_idle()`
|
|||||||
- Chrome, Firefox, Safari
|
- Chrome, Firefox, Safari
|
||||||
- Mobile browsers
|
- Mobile browsers
|
||||||
|
|
||||||
Update progress: `roboco_task_progress(task_id, "Completed visual testing...")`
|
Update progress: `roboco_task_progress(task_id, "Completed visual testing...", 50)`
|
||||||
|
|
||||||
### 6. VERDICT
|
### 6. VERDICT
|
||||||
**PASS:** `roboco_task_qa_pass(task_id, qa_notes)`
|
**PASS:** `roboco_task_qa_pass(task_id, qa_notes)`
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
|||||||
- `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, plan)` - Submit your design plan
|
||||||
- `roboco_task_progress(task_id, message)` - Update progress
|
- `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
|
||||||
- `roboco_task_pause(task_id, reason, checkpoint_summary, remaining_work)` - Pause with checkpoint
|
- `roboco_task_pause(task_id, reason, checkpoint_summary, remaining_work)` - Pause with checkpoint
|
||||||
@@ -118,7 +118,7 @@ Design work in Figma:
|
|||||||
- Create all required states (default, hover, active, focus, disabled, loading, error)
|
- Create all required states (default, hover, active, focus, disabled, loading, error)
|
||||||
- Design for all breakpoints (mobile, tablet, desktop)
|
- Design for all breakpoints (mobile, tablet, desktop)
|
||||||
- Document interactions and animations
|
- Document interactions and animations
|
||||||
- Update progress: `roboco_task_progress(task_id, "Completed mobile designs...")`
|
- Update progress: `roboco_task_progress(task_id, "Completed mobile designs...", 40)`
|
||||||
- Journal decisions: `roboco_journal_decision(data)`
|
- Journal decisions: `roboco_journal_decision(data)`
|
||||||
- Journal learnings: `roboco_journal_learning(data)`
|
- Journal learnings: `roboco_journal_learning(data)`
|
||||||
|
|
||||||
@@ -151,21 +151,38 @@ Checklist:
|
|||||||
- Edge cases handled
|
- Edge cases handled
|
||||||
|
|
||||||
### 8. NOTES & HANDOFF
|
### 8. NOTES & HANDOFF
|
||||||
|
|
||||||
|
**IMPORTANT: Two types of notes with different audiences:**
|
||||||
|
|
||||||
|
1. **Task Notes (for QA)** - Via `roboco_task_submit_qa` - QA and Documenter WILL see these
|
||||||
|
2. **Journal (personal)** - Via `roboco_journal_reflect` - Only YOU can see your journal
|
||||||
|
|
||||||
**Tool:** `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)`
|
**Tool:** `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)`
|
||||||
|
|
||||||
|
This is what QA uses to verify your work. Include:
|
||||||
|
- What you designed and where (Figma links)
|
||||||
|
- Design decisions and rationale
|
||||||
|
- All states covered (default, hover, error, loading, etc.)
|
||||||
|
- Accessibility considerations
|
||||||
|
|
||||||
```python
|
```python
|
||||||
roboco_task_submit_qa(task_id, {
|
roboco_task_submit_qa(task_id, {
|
||||||
"dev_notes": "Used segmented control for theme toggle. All states in Figma.",
|
"dev_notes": "Used segmented control for theme toggle. All states in Figma. WCAG AA compliant contrast ratios.",
|
||||||
"handoff_summary": "Figma link: [link]. Mobile-first, responsive. All states complete."
|
"handoff_summary": "Figma link: [link]. Mobile-first, responsive. All states: default, hover, active, disabled, loading."
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
**Tool:** `roboco_journal_reflect(data)`
|
**Tool:** `roboco_journal_reflect(data)` (Personal - QA cannot see this)
|
||||||
Document what you designed, decisions made, what you learned.
|
|
||||||
|
|
||||||
### 9. CLOSE
|
Document what you designed, decisions made, what you learned for your own growth.
|
||||||
- After QA approval + Documentation complete
|
|
||||||
- Task transitions to "completed" automatically
|
### 9. DONE
|
||||||
- Return to SCAN: `roboco_task_scan()` or `roboco_agent_idle()`
|
After you submit for QA, the task flows through:
|
||||||
|
1. **QA** reviews and passes/fails
|
||||||
|
2. **Documenter** writes docs
|
||||||
|
3. **Cell PM** reviews and completes
|
||||||
|
|
||||||
|
Return to SCAN: `roboco_task_scan()` or `roboco_agent_idle()`
|
||||||
|
|
||||||
## Communication Rules
|
## Communication Rules
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ You are the UX/UI Documenter at RoboCo, an AI-powered software company. You main
|
|||||||
- `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_start(task_id)` - Begin documentation work
|
- `roboco_task_start(task_id)` - Begin documentation work
|
||||||
- `roboco_task_progress(task_id, message)` - Update progress
|
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
|
||||||
- `roboco_task_complete(task_id)` - Mark documentation complete
|
- `roboco_task_docs_complete(task_id, doc_notes?)` - Mark docs done (goes to PM review)
|
||||||
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
||||||
|
|
||||||
**Journal:**
|
**Journal:**
|
||||||
@@ -106,9 +106,13 @@ If none: `roboco_agent_idle()`
|
|||||||
- {Description}
|
- {Description}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. COMPLETE
|
### 7. SUBMIT TO PM
|
||||||
`roboco_task_complete(task_id)` - Mark task as completed
|
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
|
||||||
`roboco_message_send(data)` - Announce in #uxui-cell
|
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"
|
||||||
|
|
||||||
|
**NOTE:** You do NOT complete the task. The Cell PM will review your docs
|
||||||
|
and verify all subtasks are done before calling `roboco_task_complete()`.
|
||||||
|
|
||||||
### 8. DOCUMENT
|
### 8. DOCUMENT
|
||||||
`roboco_journal_reflect(data)` - Document your documentation work
|
`roboco_journal_reflect(data)` - Document your documentation work
|
||||||
@@ -129,7 +133,7 @@ capabilities:
|
|||||||
tools:
|
tools:
|
||||||
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
||||||
- roboco_task_start, roboco_task_progress
|
- roboco_task_start, roboco_task_progress
|
||||||
- roboco_task_complete
|
- roboco_task_docs_complete # NOT roboco_task_complete (that's PM only)
|
||||||
- roboco_task_escalate, roboco_agent_idle
|
- roboco_task_escalate, roboco_agent_idle
|
||||||
- roboco_journal_entry, roboco_journal_reflect
|
- roboco_journal_entry, roboco_journal_reflect
|
||||||
- roboco_journal_decision, roboco_journal_learning
|
- roboco_journal_decision, roboco_journal_learning
|
||||||
@@ -156,6 +160,6 @@ permissions:
|
|||||||
|
|
||||||
task_permissions:
|
task_permissions:
|
||||||
- claim_doc_tasks
|
- claim_doc_tasks
|
||||||
- complete_tasks
|
- mark_docs_complete # NOT complete_tasks (that's PM only)
|
||||||
- escalate_tasks
|
- escalate_tasks
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ You interact with RoboCo systems through MCP tools:
|
|||||||
- `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, plan)` - Add your triage plan to the task
|
||||||
- `roboco_task_progress(task_id, message)` - Add progress notes
|
- `roboco_task_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||||
- `roboco_task_create(data)` - Create subtasks for designers
|
- `roboco_task_create(data)` - Create subtasks for designers
|
||||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||||
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
||||||
@@ -165,8 +165,18 @@ Tell the team what you did:
|
|||||||
- You're done with this triage
|
- You're done with this triage
|
||||||
- The orchestrator will spawn you again when needed
|
- The orchestrator will spawn you again when needed
|
||||||
|
|
||||||
## Handling Parent Task Closure
|
## Handling Task Completion (PM Review)
|
||||||
|
|
||||||
|
After documenter marks docs complete, tasks go to "awaiting_pm_review".
|
||||||
|
As the Cell PM, you review and complete these tasks:
|
||||||
|
|
||||||
|
### Simple Task Completion
|
||||||
|
1. **Scan:** `roboco_task_scan()` - find tasks in "awaiting_pm_review"
|
||||||
|
2. **Review:** `roboco_task_get(task_id)` - verify docs exist, work is satisfactory
|
||||||
|
3. **Complete:** `roboco_task_complete(task_id)` - finalize the task
|
||||||
|
4. **Notify:** `roboco_message_send()` - announce completion
|
||||||
|
|
||||||
|
### Parent Task Closure
|
||||||
When all subtasks of a parent task are completed:
|
When all subtasks of a parent task are completed:
|
||||||
|
|
||||||
1. **Review:** `roboco_task_get(parent_task_id)` - verify all subtasks done
|
1. **Review:** `roboco_task_get(parent_task_id)` - verify all subtasks done
|
||||||
@@ -174,6 +184,10 @@ When all subtasks of a parent task are completed:
|
|||||||
3. **Complete:** `roboco_task_complete(parent_task_id)` - close the parent
|
3. **Complete:** `roboco_task_complete(parent_task_id)` - close the parent
|
||||||
4. **Notify:** `roboco_message_send()` - announce completion to team
|
4. **Notify:** `roboco_message_send()` - announce completion to team
|
||||||
|
|
||||||
|
**IMPORTANT:** Only you (the PM) can call `roboco_task_complete()`.
|
||||||
|
Developers, QA, and Documenters cannot complete tasks - they prepare
|
||||||
|
the task for your final review.
|
||||||
|
|
||||||
## Cross-Cell Coordination
|
## Cross-Cell Coordination
|
||||||
|
|
||||||
### With Frontend (FE-PM)
|
### With Frontend (FE-PM)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ You are the UX/UI QA Engineer at RoboCo, an AI-powered software company. You ens
|
|||||||
- `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_start(task_id)` - Begin QA work
|
- `roboco_task_start(task_id)` - Begin QA work
|
||||||
- `roboco_task_progress(task_id, message)` - Update progress
|
- `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
|
||||||
- `roboco_task_qa_fail(task_id, qa_notes, issues)` - Reject with issues
|
- `roboco_task_qa_fail(task_id, qa_notes, issues)` - Reject with issues
|
||||||
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
||||||
@@ -74,6 +74,16 @@ If none: `roboco_agent_idle()`
|
|||||||
### 3. UNDERSTAND
|
### 3. UNDERSTAND
|
||||||
`roboco_task_get(task_id)` - Read requirements, review Figma
|
`roboco_task_get(task_id)` - Read requirements, review Figma
|
||||||
|
|
||||||
|
**What you can see:**
|
||||||
|
- `dev_notes` - Designer's work evidence and Figma links
|
||||||
|
- `progress_updates` - Timestamped progress with percentages
|
||||||
|
- Requirements and acceptance criteria
|
||||||
|
|
||||||
|
**What you CANNOT see:**
|
||||||
|
- Designer's personal journal (private)
|
||||||
|
|
||||||
|
If dev_notes is empty or no Figma link provided, that's a valid FAIL reason.
|
||||||
|
|
||||||
### 4. START
|
### 4. START
|
||||||
`roboco_task_start(task_id)` - Required before adding notes
|
`roboco_task_start(task_id)` - Required before adding notes
|
||||||
|
|
||||||
|
|||||||
+88
-80
@@ -31,8 +31,8 @@ class DeveloperAgent(Agent):
|
|||||||
4. PLAN - Break into subtasks, create plan
|
4. PLAN - Break into subtasks, create plan
|
||||||
5. EXECUTE - Work through subtasks, commit frequently
|
5. EXECUTE - Work through subtasks, commit frequently
|
||||||
6. VERIFY - Self-test, run quality checks
|
6. VERIFY - Self-test, run quality checks
|
||||||
7. NOTES - Document journey, create handoff
|
7. NOTES - Document journey, create handoff, submit for QA
|
||||||
8. CLOSE - After QA approval, mark complete
|
8. DONE - Return to SCAN (QA → Documenter → PM complete the task)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config: AgentConfig) -> None:
|
def __init__(self, config: AgentConfig) -> None:
|
||||||
@@ -96,7 +96,8 @@ class DeveloperAgent(Agent):
|
|||||||
"""
|
"""
|
||||||
Execute task through the developer lifecycle phases.
|
Execute task through the developer lifecycle phases.
|
||||||
|
|
||||||
Returns True when task is completed (after QA + docs).
|
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
|
# Initialize or restore task context
|
||||||
if self._task_context is None or self._task_context.task_id != task_id:
|
if self._task_context is None or self._task_context.task_id != task_id:
|
||||||
@@ -128,7 +129,6 @@ class DeveloperAgent(Agent):
|
|||||||
DevTaskPhase.EXECUTE: self._handle_execute_phase,
|
DevTaskPhase.EXECUTE: self._handle_execute_phase,
|
||||||
DevTaskPhase.VERIFY: self._handle_verify_phase,
|
DevTaskPhase.VERIFY: self._handle_verify_phase,
|
||||||
DevTaskPhase.NOTES: self._handle_notes_phase,
|
DevTaskPhase.NOTES: self._handle_notes_phase,
|
||||||
DevTaskPhase.CLOSE: self._handle_close_phase,
|
|
||||||
DevTaskPhase.BLOCKED: self._handle_blocked_phase,
|
DevTaskPhase.BLOCKED: self._handle_blocked_phase,
|
||||||
}
|
}
|
||||||
handler = phase_handlers.get(ctx.phase)
|
handler = phase_handlers.get(ctx.phase)
|
||||||
@@ -169,14 +169,11 @@ class DeveloperAgent(Agent):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
async def _handle_notes_phase(self, ctx: TaskContext) -> bool:
|
async def _handle_notes_phase(self, ctx: TaskContext) -> bool:
|
||||||
"""Handle NOTES phase transition."""
|
"""Handle NOTES phase transition. This is the developer's final phase."""
|
||||||
await self._phase_notes(ctx)
|
await self._phase_notes(ctx)
|
||||||
ctx.phase = DevTaskPhase.CLOSE
|
# Developer is done - task is now awaiting_qa
|
||||||
return False
|
# QA → Documenter → PM will complete the task
|
||||||
|
return True
|
||||||
async def _handle_close_phase(self, ctx: TaskContext) -> bool:
|
|
||||||
"""Handle CLOSE phase transition."""
|
|
||||||
return await self._phase_close(ctx)
|
|
||||||
|
|
||||||
async def _handle_blocked_phase(self, ctx: TaskContext) -> bool:
|
async def _handle_blocked_phase(self, ctx: TaskContext) -> bool:
|
||||||
"""Handle BLOCKED phase transition."""
|
"""Handle BLOCKED phase transition."""
|
||||||
@@ -385,11 +382,18 @@ Respond with the implementation.
|
|||||||
commit_hash = f"commit_{ctx.current_subtask}"
|
commit_hash = f"commit_{ctx.current_subtask}"
|
||||||
ctx.commits.append(commit_hash)
|
ctx.commits.append(commit_hash)
|
||||||
|
|
||||||
# Progress update
|
# Progress update - save to task AND send message
|
||||||
progress = f"{ctx.current_subtask + 1}/{len(ctx.subtasks)}"
|
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(
|
await self.send_message(
|
||||||
self._cell_channel_id or ctx.task_id,
|
self._cell_channel_id or ctx.task_id,
|
||||||
f"TASK-{str(ctx.task_id)[:8]} progress: subtask {progress} complete",
|
f"TASK-{str(ctx.task_id)[:8]} ({percentage}%) {progress_msg}",
|
||||||
message_type="action",
|
message_type="action",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -442,63 +446,47 @@ Respond with the implementation.
|
|||||||
"""
|
"""
|
||||||
NOTES phase: Document journey and create handoff.
|
NOTES phase: Document journey and create handoff.
|
||||||
|
|
||||||
- Complete journey notes
|
- Complete journey notes (stored in task dev_notes for QA)
|
||||||
- Link commits
|
- Link commits
|
||||||
- Create documenter handoff
|
- Create documenter handoff summary
|
||||||
"""
|
"""
|
||||||
self.log.info("NOTES phase", task_id=str(ctx.task_id))
|
self.log.info("NOTES phase", task_id=str(ctx.task_id))
|
||||||
|
|
||||||
# Generate handoff using LLM
|
# Generate dev_notes for QA verification (what was built, where, key decisions)
|
||||||
prompt = f"""
|
dev_notes_prompt = f"""
|
||||||
Create a documentation handoff for this completed task:
|
Summarize the work done for QA verification:
|
||||||
|
|
||||||
Task: {ctx.title}
|
Task: {ctx.title}
|
||||||
Commits: {", ".join(ctx.commits)}
|
Commits: {", ".join(ctx.commits)}
|
||||||
Journal:
|
Work log:
|
||||||
{chr(10).join(ctx.journal_entries)}
|
{chr(10).join(ctx.journal_entries)}
|
||||||
|
|
||||||
Create a handoff summary including:
|
Create a brief summary for QA including:
|
||||||
1. What was built
|
1. What was built and where (files/modules)
|
||||||
2. Key changes
|
2. Key implementation decisions
|
||||||
3. Documentation needed
|
3. Tests added
|
||||||
4. Code samples to include
|
4. Any gotchas or important context
|
||||||
"""
|
"""
|
||||||
_handoff = await self.think(prompt) # Handoff content is for documenter
|
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(
|
ctx.journal_entries.append(
|
||||||
f"[{datetime.now(UTC).isoformat()}] Handoff created for documenter"
|
f"[{datetime.now(UTC).isoformat()}] Submitted for QA with dev_notes"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update task status
|
|
||||||
await self._update_task_status(ctx.task_id, TaskStatus.AWAITING_QA)
|
|
||||||
|
|
||||||
async def _phase_close(self, ctx: TaskContext) -> bool:
|
|
||||||
"""
|
|
||||||
CLOSE phase: After QA + documentation approval.
|
|
||||||
|
|
||||||
- Verify QA approved
|
|
||||||
- Verify documentation complete
|
|
||||||
- Mark task completed
|
|
||||||
|
|
||||||
Returns True if closed, False if waiting.
|
|
||||||
"""
|
|
||||||
self.log.info("CLOSE phase", task_id=str(ctx.task_id))
|
|
||||||
|
|
||||||
# Check if QA approved (simulated - would check actual status)
|
|
||||||
qa_approved = await self._check_qa_approved(ctx.task_id)
|
|
||||||
doc_complete = await self._check_docs_complete(ctx.task_id)
|
|
||||||
|
|
||||||
if qa_approved and doc_complete:
|
|
||||||
await self._update_task_status(ctx.task_id, TaskStatus.COMPLETED)
|
|
||||||
await self.send_message(
|
|
||||||
self._cell_channel_id or ctx.task_id,
|
|
||||||
f"TASK-{str(ctx.task_id)[:8]} completed!",
|
|
||||||
message_type="action",
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def _handle_blocked(self, ctx: TaskContext) -> bool:
|
async def _handle_blocked(self, ctx: TaskContext) -> bool:
|
||||||
"""
|
"""
|
||||||
Handle blocked state.
|
Handle blocked state.
|
||||||
@@ -603,32 +591,52 @@ Create a handoff summary including:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error("Failed to update task status", error=str(e))
|
self.log.error("Failed to update task status", error=str(e))
|
||||||
|
|
||||||
async def _check_qa_approved(self, task_id: UUID) -> bool:
|
async def _add_progress(self, task_id: UUID, message: str, percentage: int) -> None:
|
||||||
"""Check if QA has approved the task."""
|
"""
|
||||||
try:
|
Add progress update to task.
|
||||||
result = await self._api_call("GET", f"/tasks/{task_id}")
|
|
||||||
status = result.get("status", "")
|
|
||||||
# QA approved if status moved past awaiting_qa
|
|
||||||
return status in ["awaiting_documentation", "completed"]
|
|
||||||
except Exception as e:
|
|
||||||
self.log.warning("Failed to check QA status", error=str(e))
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def _check_docs_complete(self, task_id: UUID) -> bool:
|
This is saved to task.progress_updates and visible to QA.
|
||||||
"""Check if documentation is complete."""
|
Percentage is required (0-100) to show real progress.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
result = await self._api_call("GET", f"/tasks/{task_id}/handoffs")
|
await self._api_call(
|
||||||
handoffs = result.get("items", [])
|
"POST",
|
||||||
# Check if documenter handoff is complete
|
f"/tasks/{task_id}/progress",
|
||||||
for handoff in handoffs:
|
json={
|
||||||
is_doc = handoff.get("type") == "documentation"
|
"message": message,
|
||||||
is_done = handoff.get("status") == "completed"
|
"percentage": percentage,
|
||||||
if is_doc and is_done:
|
},
|
||||||
return True
|
)
|
||||||
return False
|
self.log.info("Progress saved", task_id=str(task_id), percentage=percentage)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.warning("Failed to check docs status", error=str(e))
|
self.log.warning("Failed to save progress", error=str(e))
|
||||||
return False
|
|
||||||
|
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 # Re-raise so caller knows submission failed
|
||||||
|
|
||||||
|
|
||||||
def create_backend_developer(
|
def create_backend_developer(
|
||||||
|
|||||||
@@ -404,12 +404,12 @@ good,complete,clear,helpful,None
|
|||||||
"Failed to publish", path=doc_spec.path, error=str(e)
|
"Failed to publish", path=doc_spec.path, error=str(e)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update task status
|
# Mark docs complete - task goes to PM for final review
|
||||||
await self._update_task_status(ctx.task_id, TaskStatus.COMPLETED)
|
await self._update_task_status(ctx.task_id, TaskStatus.AWAITING_PM_REVIEW)
|
||||||
|
|
||||||
await self.send_message(
|
await self.send_message(
|
||||||
self._cell_channel_id or ctx.task_id,
|
self._cell_channel_id or ctx.task_id,
|
||||||
f"TASK-{str(ctx.task_id)[:8]} documentation complete\n"
|
f"TASK-{str(ctx.task_id)[:8]} documentation complete, awaiting PM review\n"
|
||||||
f"Published: {', '.join(ctx.written_docs)}",
|
f"Published: {', '.join(ctx.written_docs)}",
|
||||||
message_type="action",
|
message_type="action",
|
||||||
)
|
)
|
||||||
@@ -446,11 +446,25 @@ good,complete,clear,helpful,None
|
|||||||
return f"Task {str(task_id)[:8]}"
|
return f"Task {str(task_id)[:8]}"
|
||||||
|
|
||||||
async def _read_dev_notes(self, task_id: UUID) -> str:
|
async def _read_dev_notes(self, task_id: UUID) -> str:
|
||||||
"""Read developer's journey notes."""
|
"""Read developer's journey notes (dev_notes + progress_updates)."""
|
||||||
try:
|
try:
|
||||||
result = await self._api_call("GET", f"/tasks/{task_id}")
|
result = await self._api_call("GET", f"/tasks/{task_id}")
|
||||||
notes: str = result.get("dev_notes", "No developer notes available")
|
notes: str = result.get("dev_notes") or ""
|
||||||
return notes
|
|
||||||
|
# Also include progress updates as they contain developer's work log
|
||||||
|
progress_updates = result.get("progress_updates", [])
|
||||||
|
if progress_updates:
|
||||||
|
progress_text = "\n".join(
|
||||||
|
f"[{u.get('timestamp', 'N/A')}] ({u.get('percentage', 0)}%) "
|
||||||
|
f"{u.get('message', '')}"
|
||||||
|
for u in progress_updates
|
||||||
|
)
|
||||||
|
if notes:
|
||||||
|
notes = f"{notes}\n\nProgress Updates:\n{progress_text}"
|
||||||
|
else:
|
||||||
|
notes = f"Progress Updates:\n{progress_text}"
|
||||||
|
|
||||||
|
return notes if notes else "No developer notes available"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.warning("Failed to read dev notes", error=str(e))
|
self.log.warning("Failed to read dev notes", error=str(e))
|
||||||
return "Dev notes unavailable"
|
return "Dev notes unavailable"
|
||||||
|
|||||||
+17
-3
@@ -453,11 +453,25 @@ PASS,All criteria verified successfully,No issues found
|
|||||||
return "Requirements unavailable"
|
return "Requirements unavailable"
|
||||||
|
|
||||||
async def _read_dev_notes(self, task_id: UUID) -> str:
|
async def _read_dev_notes(self, task_id: UUID) -> str:
|
||||||
"""Read developer's journey notes."""
|
"""Read developer's journey notes (dev_notes + progress_updates)."""
|
||||||
try:
|
try:
|
||||||
result = await self._api_call("GET", f"/tasks/{task_id}")
|
result = await self._api_call("GET", f"/tasks/{task_id}")
|
||||||
notes: str = result.get("dev_notes", "No developer notes available")
|
notes: str = result.get("dev_notes") or ""
|
||||||
return notes
|
|
||||||
|
# Also include progress updates as they contain developer's work log
|
||||||
|
progress_updates = result.get("progress_updates", [])
|
||||||
|
if progress_updates:
|
||||||
|
progress_text = "\n".join(
|
||||||
|
f"[{u.get('timestamp', 'N/A')}] ({u.get('percentage', 0)}%) "
|
||||||
|
f"{u.get('message', '')}"
|
||||||
|
for u in progress_updates
|
||||||
|
)
|
||||||
|
if notes:
|
||||||
|
notes = f"{notes}\n\nProgress Updates:\n{progress_text}"
|
||||||
|
else:
|
||||||
|
notes = f"Progress Updates:\n{progress_text}"
|
||||||
|
|
||||||
|
return notes if notes else "No developer notes available"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.warning("Failed to read dev notes", error=str(e))
|
self.log.warning("Failed to read dev notes", error=str(e))
|
||||||
return "Dev notes unavailable"
|
return "Dev notes unavailable"
|
||||||
|
|||||||
@@ -188,6 +188,21 @@ def can_assign_tasks(agent_id: str) -> bool:
|
|||||||
return role in PM_ROLES
|
return role in PM_ROLES
|
||||||
|
|
||||||
|
|
||||||
|
# Cancel roles match task_lifecycle.py - CEO and Auditor cannot cancel (they observe)
|
||||||
|
_CANCEL_ROLES: Final[set[str]] = {
|
||||||
|
"cell_pm",
|
||||||
|
"main_pm",
|
||||||
|
"product_owner",
|
||||||
|
"head_marketing",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def can_cancel_tasks(agent_id: str) -> bool:
|
||||||
|
"""Check if agent can cancel tasks (PMs and board, not CEO/Auditor)."""
|
||||||
|
role = get_agent_role(agent_id)
|
||||||
|
return role in _CANCEL_ROLES
|
||||||
|
|
||||||
|
|
||||||
def get_escalation_target(agent_id: str) -> str | None:
|
def get_escalation_target(agent_id: str) -> str | None:
|
||||||
"""Get the escalation target for an agent."""
|
"""Get the escalation target for an agent."""
|
||||||
return ESCALATION_CHAIN.get(agent_id)
|
return ESCALATION_CHAIN.get(agent_id)
|
||||||
|
|||||||
+111
-10
@@ -22,6 +22,7 @@ from roboco.api.schemas.tasks import (
|
|||||||
ListTasksQuery,
|
ListTasksQuery,
|
||||||
ProgressRequest,
|
ProgressRequest,
|
||||||
QANotes,
|
QANotes,
|
||||||
|
SoftBlockRequest,
|
||||||
TaskCountResponse,
|
TaskCountResponse,
|
||||||
TaskResponse,
|
TaskResponse,
|
||||||
TaskUpdate,
|
TaskUpdate,
|
||||||
@@ -528,6 +529,51 @@ async def block_task(
|
|||||||
return task_to_response(task)
|
return task_to_response(task)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{task_id}/soft-block", response_model=TaskResponse)
|
||||||
|
async def soft_block_task(
|
||||||
|
task_id: UUID,
|
||||||
|
data: SoftBlockRequest,
|
||||||
|
db: DbSession,
|
||||||
|
agent: CurrentAgentContext,
|
||||||
|
) -> TaskResponse:
|
||||||
|
"""Soft-block a task due to an external factor (not a task dependency).
|
||||||
|
|
||||||
|
Use this when blocked by:
|
||||||
|
- External dependencies (waiting for API access, credentials)
|
||||||
|
- Questions that need PM/stakeholder input
|
||||||
|
- Technical blockers (infrastructure issues)
|
||||||
|
|
||||||
|
For blocking due to another task, use the /block endpoint instead.
|
||||||
|
"""
|
||||||
|
service = get_task_service(db)
|
||||||
|
task = await service.get(task_id)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only assigned agent or PM can block a task
|
||||||
|
if task.assigned_to != agent.agent_id and agent.role.value not in (
|
||||||
|
"cell_pm",
|
||||||
|
"main_pm",
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not authorized to block this task",
|
||||||
|
)
|
||||||
|
|
||||||
|
task = await service.soft_block(
|
||||||
|
task_id, data.reason, data.blocker_type, data.what_needed
|
||||||
|
)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Cannot block task - must be in_progress",
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
return task_to_response(task)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{task_id}/unblock", response_model=TaskResponse)
|
@router.post("/{task_id}/unblock", response_model=TaskResponse)
|
||||||
async def unblock_task(
|
async def unblock_task(
|
||||||
task_id: UUID,
|
task_id: UUID,
|
||||||
@@ -787,14 +833,18 @@ async def fail_qa(
|
|||||||
return task_to_response(task)
|
return task_to_response(task)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{task_id}/complete", response_model=TaskResponse)
|
@router.post("/{task_id}/docs-complete", response_model=TaskResponse)
|
||||||
async def complete_task(
|
async def docs_complete(
|
||||||
task_id: UUID,
|
task_id: UUID,
|
||||||
db: DbSession,
|
db: DbSession,
|
||||||
agent: CurrentAgentContext,
|
agent: CurrentAgentContext,
|
||||||
permissions: PermissionServiceDep,
|
data: QANotes | None = None,
|
||||||
) -> TaskResponse:
|
) -> TaskResponse:
|
||||||
"""Mark task as completed."""
|
"""Mark documentation as complete (documenter only).
|
||||||
|
|
||||||
|
Transitions task from awaiting_documentation to awaiting_pm_review.
|
||||||
|
The Cell PM will then review and complete the task.
|
||||||
|
"""
|
||||||
service = get_task_service(db)
|
service = get_task_service(db)
|
||||||
task = await service.get(task_id)
|
task = await service.get(task_id)
|
||||||
if not task:
|
if not task:
|
||||||
@@ -802,14 +852,65 @@ async def complete_task(
|
|||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
|
status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check close permission - assigned agent or those with CLOSE permission
|
# Only documenter role can mark docs complete
|
||||||
is_assigned = task.assigned_to == agent.agent_id
|
if agent.role.value != "documenter":
|
||||||
can_close = permissions.can_perform_task_action(agent, TaskAction.CLOSE, task.team)
|
|
||||||
|
|
||||||
if not (is_assigned or can_close):
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not authorized to complete this task",
|
detail="Only documenters can mark documentation as complete",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Documenter cannot document their own work (self-review prevention)
|
||||||
|
original_dev = extract_original_developer(task.quick_context)
|
||||||
|
if original_dev and str(agent.agent_id) == original_dev:
|
||||||
|
audit = get_audit_service()
|
||||||
|
await audit.log_task_action_denial(
|
||||||
|
agent_id=agent.agent_id,
|
||||||
|
agent_role=agent.role.value,
|
||||||
|
task_id=task_id,
|
||||||
|
action="docs_complete",
|
||||||
|
reason="Self-documentation not permitted",
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Cannot document your own task",
|
||||||
|
)
|
||||||
|
|
||||||
|
doc_notes = data.notes if data else None
|
||||||
|
task = await service.docs_complete(task_id, doc_notes)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Cannot mark docs complete - task not awaiting documentation",
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
return task_to_response(task)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{task_id}/complete", response_model=TaskResponse)
|
||||||
|
async def complete_task(
|
||||||
|
task_id: UUID,
|
||||||
|
db: DbSession,
|
||||||
|
agent: CurrentAgentContext,
|
||||||
|
permissions: PermissionServiceDep,
|
||||||
|
) -> TaskResponse:
|
||||||
|
"""Mark task as completed (PM only).
|
||||||
|
|
||||||
|
Only PMs can complete tasks, and only from awaiting_pm_review status.
|
||||||
|
This ensures the full workflow: Dev → QA → Documenter → PM.
|
||||||
|
"""
|
||||||
|
service = get_task_service(db)
|
||||||
|
task = await service.get(task_id)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only PMs can complete tasks
|
||||||
|
can_close = permissions.can_perform_task_action(agent, TaskAction.CLOSE, task.team)
|
||||||
|
if not can_close:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Only PMs can complete tasks",
|
||||||
)
|
)
|
||||||
|
|
||||||
task = await service.complete(task_id)
|
task = await service.complete(task_id)
|
||||||
|
|||||||
@@ -289,6 +289,16 @@ class QANotes(BaseModel):
|
|||||||
notes: str
|
notes: str
|
||||||
|
|
||||||
|
|
||||||
|
class SoftBlockRequest(BaseModel):
|
||||||
|
"""Request to soft-block a task due to an external factor."""
|
||||||
|
|
||||||
|
reason: str = Field(..., description="Why the task is blocked")
|
||||||
|
blocker_type: str = Field(
|
||||||
|
..., description="Type of blocker: external, internal, question, dependency"
|
||||||
|
)
|
||||||
|
what_needed: str = Field(..., description="What is needed to unblock the task")
|
||||||
|
|
||||||
|
|
||||||
class TaskCountResponse(BaseModel):
|
class TaskCountResponse(BaseModel):
|
||||||
"""Task count by category."""
|
"""Task count by category."""
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ VALID_TRANSITIONS: dict[str, list[str]] = {
|
|||||||
"needs_revision": ["in_progress", "cancelled"],
|
"needs_revision": ["in_progress", "cancelled"],
|
||||||
# Awaiting QA - can pass (to docs), fail (needs revision), block, or cancel
|
# Awaiting QA - can pass (to docs), fail (needs revision), block, or cancel
|
||||||
"awaiting_qa": ["awaiting_documentation", "needs_revision", "blocked", "cancelled"],
|
"awaiting_qa": ["awaiting_documentation", "needs_revision", "blocked", "cancelled"],
|
||||||
# Awaiting documentation - can complete or cancel
|
# Awaiting documentation - documenter marks docs done, goes to PM review
|
||||||
"awaiting_documentation": ["completed", "cancelled"],
|
"awaiting_documentation": ["awaiting_pm_review", "cancelled"],
|
||||||
|
# Awaiting PM review - PM reviews and completes, or cancels
|
||||||
|
"awaiting_pm_review": ["completed", "cancelled"],
|
||||||
# Terminal states - cannot transition out
|
# Terminal states - cannot transition out
|
||||||
"completed": [],
|
"completed": [],
|
||||||
"cancelled": [],
|
"cancelled": [],
|
||||||
@@ -57,6 +59,10 @@ ROLE_RESTRICTED_TRANSITIONS: dict[tuple[str, str], list[str]] = {
|
|||||||
# Only QA can pass or fail QA
|
# Only QA can pass or fail 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
|
||||||
|
("awaiting_documentation", "awaiting_pm_review"): ["documenter"],
|
||||||
|
# Only PM can complete after PM review
|
||||||
|
("awaiting_pm_review", "completed"): _CANCEL_ROLES, # PMs complete tasks
|
||||||
# Only PM or higher can cancel tasks (all states that allow cancel)
|
# Only PM or higher can cancel tasks (all states that allow cancel)
|
||||||
("pending", "cancelled"): _CANCEL_ROLES,
|
("pending", "cancelled"): _CANCEL_ROLES,
|
||||||
("claimed", "cancelled"): _CANCEL_ROLES,
|
("claimed", "cancelled"): _CANCEL_ROLES,
|
||||||
@@ -67,6 +73,7 @@ ROLE_RESTRICTED_TRANSITIONS: dict[tuple[str, str], list[str]] = {
|
|||||||
("needs_revision", "cancelled"): _CANCEL_ROLES,
|
("needs_revision", "cancelled"): _CANCEL_ROLES,
|
||||||
("awaiting_qa", "cancelled"): _CANCEL_ROLES,
|
("awaiting_qa", "cancelled"): _CANCEL_ROLES,
|
||||||
("awaiting_documentation", "cancelled"): _CANCEL_ROLES,
|
("awaiting_documentation", "cancelled"): _CANCEL_ROLES,
|
||||||
|
("awaiting_pm_review", "cancelled"): _CANCEL_ROLES,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -155,7 +162,13 @@ def is_terminal_state(status: str) -> bool:
|
|||||||
|
|
||||||
def is_waiting_state(status: str) -> bool:
|
def is_waiting_state(status: str) -> bool:
|
||||||
"""Check if a status is a waiting state (agent can work on other tasks)."""
|
"""Check if a status is a waiting state (agent can work on other tasks)."""
|
||||||
return status in ("blocked", "paused", "awaiting_qa", "awaiting_documentation")
|
return status in (
|
||||||
|
"blocked",
|
||||||
|
"paused",
|
||||||
|
"awaiting_qa",
|
||||||
|
"awaiting_documentation",
|
||||||
|
"awaiting_pm_review",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_active_state(status: str) -> bool:
|
def is_active_state(status: str) -> bool:
|
||||||
|
|||||||
+68
-60
@@ -4,11 +4,18 @@ Notify MCP Server
|
|||||||
Exposes notification tools to Claude Code agents with built-in
|
Exposes notification tools to Claude Code agents with built-in
|
||||||
enforcement of notification permissions.
|
enforcement of notification permissions.
|
||||||
|
|
||||||
Tools:
|
Tools available to ALL agents:
|
||||||
- roboco_notify_list: List your notifications
|
- roboco_notify_list: List your notifications
|
||||||
- roboco_notify_get: Get a specific notification
|
- roboco_notify_get: Get a specific notification
|
||||||
- roboco_notify_ack: Acknowledge a notification
|
- roboco_notify_ack: Acknowledge a notification
|
||||||
- roboco_notify_send: Send a notification (PM/Board/Auditor only)
|
|
||||||
|
Tools available ONLY to PM/Board/Auditor:
|
||||||
|
- roboco_notify_send: Send a notification
|
||||||
|
- roboco_escalate: Escalate an issue (PMs only)
|
||||||
|
- roboco_request_approval: Request approval (PMs/Board only)
|
||||||
|
|
||||||
|
Note: Developers, QA, and Documenters do not see the sending tools.
|
||||||
|
They should use message channels and blocker reporting instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -18,6 +25,7 @@ from mcp.server.fastmcp import FastMCP
|
|||||||
|
|
||||||
from roboco.agents_config import (
|
from roboco.agents_config import (
|
||||||
NOTIFICATION_PERMISSIONS,
|
NOTIFICATION_PERMISSIONS,
|
||||||
|
can_send_notifications,
|
||||||
get_agent_cell,
|
get_agent_cell,
|
||||||
get_agent_role,
|
get_agent_role,
|
||||||
)
|
)
|
||||||
@@ -301,71 +309,71 @@ def create_notify_mcp_server(agent_id: str) -> FastMCP:
|
|||||||
"""Acknowledge a notification."""
|
"""Acknowledge a notification."""
|
||||||
return await _handle_ack(client, notification_id)
|
return await _handle_ack(client, notification_id)
|
||||||
|
|
||||||
@mcp.tool()
|
# Only register send/escalate/approval tools for agents who can send notifications
|
||||||
async def roboco_notify_send(data: SendNotificationInput) -> dict[str, Any]:
|
# This prevents developers, QA, and documenters from even seeing these tools
|
||||||
"""
|
if can_send_notifications(agent_id):
|
||||||
Send a notification to one or more agents.
|
|
||||||
|
|
||||||
Only PMs, Board members, and Auditor can send notifications.
|
@mcp.tool()
|
||||||
Cell PMs can only notify their own cell.
|
async def roboco_notify_send(data: SendNotificationInput) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
return await _handle_send(client, agent_id, data)
|
Send a notification to one or more agents.
|
||||||
|
|
||||||
@mcp.tool()
|
Cell PMs can only notify their own cell.
|
||||||
async def roboco_escalate(
|
Main PM, Board, and Auditor can notify anyone.
|
||||||
escalate_to: str,
|
"""
|
||||||
subject: str,
|
return await _handle_send(client, agent_id, data)
|
||||||
description: str,
|
|
||||||
task_id: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Escalate an issue to a higher level (PM only).
|
|
||||||
|
|
||||||
Sends a high-priority notification requiring acknowledgment.
|
|
||||||
"""
|
|
||||||
role = get_agent_role(agent_id)
|
role = get_agent_role(agent_id)
|
||||||
if role not in ["cell_pm", "main_pm"]:
|
|
||||||
return format_error_response(
|
|
||||||
"NOT_PM", "Only PMs can use the escalate function"
|
|
||||||
)
|
|
||||||
|
|
||||||
input_data = SendNotificationInput(
|
# Only PMs can escalate
|
||||||
recipients=[escalate_to],
|
if role in ["cell_pm", "main_pm"]:
|
||||||
subject=f"[ESCALATION] {subject}",
|
|
||||||
body=description,
|
|
||||||
notification_type="escalation",
|
|
||||||
priority="high",
|
|
||||||
requires_ack=True,
|
|
||||||
related_task_id=task_id,
|
|
||||||
)
|
|
||||||
return await _handle_send(client, agent_id, input_data)
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_request_approval(
|
async def roboco_escalate(
|
||||||
approver: str,
|
escalate_to: str,
|
||||||
subject: str,
|
subject: str,
|
||||||
what_needs_approval: str,
|
description: str,
|
||||||
task_id: str | None = None,
|
task_id: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Request approval from someone (PM/Board only).
|
Escalate an issue to a higher level.
|
||||||
"""
|
|
||||||
role = get_agent_role(agent_id)
|
|
||||||
if role not in ["cell_pm", "main_pm", "product_owner", "head_marketing"]:
|
|
||||||
return format_error_response(
|
|
||||||
"NOT_AUTHORIZED", "Only PMs and Board can request approvals"
|
|
||||||
)
|
|
||||||
|
|
||||||
input_data = SendNotificationInput(
|
Sends a high-priority notification requiring acknowledgment.
|
||||||
recipients=[approver],
|
"""
|
||||||
subject=f"[APPROVAL NEEDED] {subject}",
|
input_data = SendNotificationInput(
|
||||||
body=what_needs_approval,
|
recipients=[escalate_to],
|
||||||
notification_type="approval",
|
subject=f"[ESCALATION] {subject}",
|
||||||
priority="normal",
|
body=description,
|
||||||
requires_ack=True,
|
notification_type="escalation",
|
||||||
related_task_id=task_id,
|
priority="high",
|
||||||
)
|
requires_ack=True,
|
||||||
return await _handle_send(client, agent_id, input_data)
|
related_task_id=task_id,
|
||||||
|
)
|
||||||
|
return await _handle_send(client, agent_id, input_data)
|
||||||
|
|
||||||
|
# Only PMs and Board can request approvals
|
||||||
|
if role in ["cell_pm", "main_pm", "product_owner", "head_marketing"]:
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def roboco_request_approval(
|
||||||
|
approver: str,
|
||||||
|
subject: str,
|
||||||
|
what_needs_approval: str,
|
||||||
|
task_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Request approval from someone.
|
||||||
|
"""
|
||||||
|
input_data = SendNotificationInput(
|
||||||
|
recipients=[approver],
|
||||||
|
subject=f"[APPROVAL NEEDED] {subject}",
|
||||||
|
body=what_needs_approval,
|
||||||
|
notification_type="approval",
|
||||||
|
priority="normal",
|
||||||
|
requires_ack=True,
|
||||||
|
related_task_id=task_id,
|
||||||
|
)
|
||||||
|
return await _handle_send(client, agent_id, input_data)
|
||||||
|
|
||||||
return mcp
|
return mcp
|
||||||
|
|
||||||
|
|||||||
+326
-104
@@ -17,9 +17,11 @@ Tools:
|
|||||||
- roboco_task_submit_qa: Submit for QA review
|
- roboco_task_submit_qa: Submit for QA review
|
||||||
- roboco_task_qa_pass: Pass QA (QA role only)
|
- roboco_task_qa_pass: Pass QA (QA role only)
|
||||||
- roboco_task_qa_fail: Fail QA (QA role only)
|
- roboco_task_qa_fail: Fail QA (QA role only)
|
||||||
- roboco_task_complete: Mark task complete
|
- roboco_task_docs_complete: Mark docs complete (Documenter only)
|
||||||
|
- roboco_task_complete: Mark task complete (PM only, after docs)
|
||||||
- roboco_task_create: Create new task (PM only)
|
- roboco_task_create: Create new task (PM only)
|
||||||
- roboco_task_assign: Assign task to agent (PM only)
|
- roboco_task_assign: Assign task to agent (PM only)
|
||||||
|
- roboco_task_cancel: Cancel a task (PM/Board only)
|
||||||
- roboco_task_escalate: Escalate task up hierarchy (all agents)
|
- roboco_task_escalate: Escalate task up hierarchy (all agents)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ from mcp.server.fastmcp import FastMCP
|
|||||||
|
|
||||||
from roboco.agents_config import (
|
from roboco.agents_config import (
|
||||||
can_assign_tasks,
|
can_assign_tasks,
|
||||||
|
can_cancel_tasks,
|
||||||
can_create_tasks,
|
can_create_tasks,
|
||||||
get_agent_role,
|
get_agent_role,
|
||||||
get_agent_team,
|
get_agent_team,
|
||||||
@@ -70,6 +73,10 @@ async def _resolve_agent_uuid_cached(agent_id: str, client: ApiClient) -> str |
|
|||||||
# Global TOON adapter for encoding task data
|
# Global TOON adapter for encoding task data
|
||||||
_toon = ToonAdapter()
|
_toon = ToonAdapter()
|
||||||
|
|
||||||
|
# Progress percentage bounds
|
||||||
|
_MIN_PERCENTAGE = 0
|
||||||
|
_MAX_PERCENTAGE = 100
|
||||||
|
|
||||||
# NOTE: For task lifecycle validation, use enforcement.task_lifecycle.VALID_TRANSITIONS
|
# NOTE: For task lifecycle validation, use enforcement.task_lifecycle.VALID_TRANSITIONS
|
||||||
|
|
||||||
|
|
||||||
@@ -166,6 +173,57 @@ def _get_next_step_guidance(status: str) -> tuple[str, str]:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _get_available_tasks_guidance(
|
||||||
|
available_tasks: list[dict[str, Any]], agent_role: str
|
||||||
|
) -> str:
|
||||||
|
"""Generate guidance for available tasks based on agent role."""
|
||||||
|
review_count = sum(
|
||||||
|
1 for t in available_tasks if t.get("status") == "awaiting_pm_review"
|
||||||
|
)
|
||||||
|
pending_count = len(available_tasks) - review_count
|
||||||
|
|
||||||
|
if agent_role in ("cell_pm", "main_pm") and review_count > 0:
|
||||||
|
return (
|
||||||
|
f"Found {review_count} task(s) awaiting your review. "
|
||||||
|
"Use roboco_task_get to review, then roboco_task_complete to finalize. "
|
||||||
|
f"Also {pending_count} pending task(s) need triage."
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"Found {len(available_tasks)} available task(s). "
|
||||||
|
"Review and claim one that matches your skills."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_available_tasks_for_role(
|
||||||
|
client: ApiClient, agent_role: str, team: str | None
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Get available tasks based on agent role."""
|
||||||
|
params = {"team": team} if team else {}
|
||||||
|
|
||||||
|
if agent_role == "qa":
|
||||||
|
resp = await client.get("/tasks/awaiting-qa", params=params)
|
||||||
|
return resp.json() if resp.ok else []
|
||||||
|
|
||||||
|
if agent_role == "documenter":
|
||||||
|
resp = await client.get("/tasks/awaiting-docs", params=params)
|
||||||
|
return resp.json() if resp.ok else []
|
||||||
|
|
||||||
|
if agent_role in ("cell_pm", "main_pm"):
|
||||||
|
# PMs get pending tasks AND tasks awaiting their review
|
||||||
|
pending_params = {**params, "status": "pending"}
|
||||||
|
pending_resp = await client.get("/tasks", params=pending_params)
|
||||||
|
pending = pending_resp.json() if pending_resp.ok else []
|
||||||
|
review_resp = await client.get(
|
||||||
|
"/tasks", params={**params, "status": "awaiting_pm_review"}
|
||||||
|
)
|
||||||
|
review = review_resp.json() if review_resp.ok else []
|
||||||
|
return pending + review
|
||||||
|
|
||||||
|
# Developers get pending tasks only
|
||||||
|
resp = await client.get("/tasks", params={**params, "status": "pending"})
|
||||||
|
return resp.json() if resp.ok else []
|
||||||
|
|
||||||
|
|
||||||
async def _handle_task_scan(
|
async def _handle_task_scan(
|
||||||
client: ApiClient, team: str | None, agent_id: str
|
client: ApiClient, team: str | None, agent_id: str
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -186,42 +244,15 @@ async def _handle_task_scan(
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Get available tasks based on agent role
|
# Get available tasks based on agent role
|
||||||
# QA agents need awaiting_qa tasks, Documenters need awaiting_documentation
|
|
||||||
agent_role = get_agent_role(agent_id)
|
agent_role = get_agent_role(agent_id)
|
||||||
|
available_tasks = await _get_available_tasks_for_role(client, agent_role, team)
|
||||||
available_tasks: list[dict[str, Any]] = []
|
|
||||||
|
|
||||||
if agent_role == "qa":
|
|
||||||
# QA agents look for tasks awaiting QA review
|
|
||||||
qa_resp = await client.get(
|
|
||||||
"/tasks/awaiting-qa",
|
|
||||||
params={"team": team} if team else {},
|
|
||||||
)
|
|
||||||
if qa_resp.ok:
|
|
||||||
available_tasks = qa_resp.json()
|
|
||||||
elif agent_role == "documenter":
|
|
||||||
# Documenters look for tasks awaiting documentation
|
|
||||||
doc_resp = await client.get(
|
|
||||||
"/tasks/awaiting-docs",
|
|
||||||
params={"team": team} if team else {},
|
|
||||||
)
|
|
||||||
if doc_resp.ok:
|
|
||||||
available_tasks = doc_resp.json()
|
|
||||||
else:
|
|
||||||
# Developers and PMs look for pending tasks
|
|
||||||
params: dict[str, Any] = {"status": "pending"}
|
|
||||||
if team:
|
|
||||||
params["team"] = team
|
|
||||||
pending_resp = await client.get("/tasks", params=params)
|
|
||||||
if pending_resp.ok:
|
|
||||||
available_tasks = pending_resp.json()
|
|
||||||
|
|
||||||
# Filter out tasks already in assigned_tasks from available_tasks
|
# Filter out tasks already in assigned_tasks from available_tasks
|
||||||
# (prevents PM-assigned pending tasks from appearing in both lists)
|
# (prevents PM-assigned pending tasks from appearing in both lists)
|
||||||
assigned_ids = {t.get("id") for t in assigned_tasks}
|
assigned_ids = {t.get("id") for t in assigned_tasks}
|
||||||
available_tasks = [t for t in available_tasks if t.get("id") not in assigned_ids]
|
available_tasks = [t for t in available_tasks if t.get("id") not in assigned_ids]
|
||||||
|
|
||||||
# Determine guidance
|
# Determine guidance based on role and available tasks
|
||||||
if paused_tasks:
|
if paused_tasks:
|
||||||
guidance = (
|
guidance = (
|
||||||
f"You have {len(paused_tasks)} paused task(s). "
|
f"You have {len(paused_tasks)} paused task(s). "
|
||||||
@@ -233,10 +264,7 @@ async def _handle_task_scan(
|
|||||||
"Continue working on your assigned tasks."
|
"Continue working on your assigned tasks."
|
||||||
)
|
)
|
||||||
elif available_tasks:
|
elif available_tasks:
|
||||||
guidance = (
|
guidance = _get_available_tasks_guidance(available_tasks, agent_role)
|
||||||
f"Found {len(available_tasks)} available task(s). "
|
|
||||||
"Review and claim one that matches your skills."
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
guidance = (
|
guidance = (
|
||||||
"No tasks available. Call roboco_agent_idle() "
|
"No tasks available. Call roboco_agent_idle() "
|
||||||
@@ -492,30 +520,40 @@ async def _validate_task_start(
|
|||||||
return error
|
return error
|
||||||
|
|
||||||
task_status = task.get("status")
|
task_status = task.get("status")
|
||||||
if task_status not in ["claimed", "paused"]:
|
# Valid statuses to start/resume work:
|
||||||
|
# - claimed: Developer just claimed a pending task
|
||||||
|
# - paused: Developer resuming paused work
|
||||||
|
# - needs_revision: Developer resuming after QA rejection
|
||||||
|
valid_start_statuses = ["claimed", "paused", "needs_revision"]
|
||||||
|
if task_status not in valid_start_statuses:
|
||||||
return _format_error_response(
|
return _format_error_response(
|
||||||
"INVALID_STATE",
|
"INVALID_STATE",
|
||||||
f"Cannot start task in '{task_status}' status. "
|
f"Cannot start task in '{task_status}' status. "
|
||||||
"Task must be 'claimed' or 'paused'.",
|
"Task must be 'claimed', 'paused', or 'needs_revision'.",
|
||||||
{"current_status": task_status},
|
{"current_status": task_status},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Only require plan for newly claimed tasks, not for resuming revision
|
||||||
if task_status == "claimed" and not task.get("plan"):
|
if task_status == "claimed" and not task.get("plan"):
|
||||||
return _format_error_response(
|
return _format_error_response(
|
||||||
"NO_PLAN",
|
"NO_PLAN",
|
||||||
"Cannot start without a plan. Call roboco_task_plan first.",
|
"Cannot start without a plan. Call roboco_task_plan first.",
|
||||||
)
|
)
|
||||||
|
|
||||||
plan = task.get("plan", {})
|
# Only check open questions for newly claimed tasks
|
||||||
unanswered = [q for q in plan.get("open_questions", []) if not q.get("answered")]
|
if task_status == "claimed":
|
||||||
if unanswered:
|
plan = task.get("plan", {})
|
||||||
return _format_error_response(
|
unanswered = [
|
||||||
"UNANSWERED_QUESTIONS",
|
q for q in plan.get("open_questions", []) if not q.get("answered")
|
||||||
f"Cannot start with {len(unanswered)} "
|
]
|
||||||
"unanswered question(s). "
|
if unanswered:
|
||||||
"Get answers first, then update the plan.",
|
return _format_error_response(
|
||||||
{"questions": [q.get("question") for q in unanswered]},
|
"UNANSWERED_QUESTIONS",
|
||||||
)
|
f"Cannot start with {len(unanswered)} "
|
||||||
|
"unanswered question(s). "
|
||||||
|
"Get answers first, then update the plan.",
|
||||||
|
{"questions": [q.get("question") for q in unanswered]},
|
||||||
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -559,10 +597,17 @@ async def _handle_task_progress(
|
|||||||
client: ApiClient,
|
client: ApiClient,
|
||||||
task_id: str,
|
task_id: str,
|
||||||
message: str,
|
message: str,
|
||||||
percentage: int | None,
|
percentage: int,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Handle task progress update."""
|
"""Handle task progress update."""
|
||||||
|
# Validate percentage is in valid range
|
||||||
|
if not _MIN_PERCENTAGE <= percentage <= _MAX_PERCENTAGE:
|
||||||
|
return _format_error_response(
|
||||||
|
"INVALID_PERCENTAGE",
|
||||||
|
f"Percentage must be between {_MIN_PERCENTAGE} and {_MAX_PERCENTAGE}",
|
||||||
|
)
|
||||||
|
|
||||||
task_resp = await client.get(f"/tasks/{task_id}")
|
task_resp = await client.get(f"/tasks/{task_id}")
|
||||||
if task_resp.is_status(status.HTTP_404_NOT_FOUND):
|
if task_resp.is_status(status.HTTP_404_NOT_FOUND):
|
||||||
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
|
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
|
||||||
@@ -572,10 +617,17 @@ async def _handle_task_progress(
|
|||||||
if error := await _validate_task_ownership(task, agent_id, client):
|
if error := await _validate_task_ownership(task, agent_id, client):
|
||||||
return error
|
return error
|
||||||
|
|
||||||
if task.get("status") != "in_progress":
|
# Allow progress updates for active work statuses
|
||||||
|
active_statuses = {
|
||||||
|
"in_progress",
|
||||||
|
"verifying",
|
||||||
|
"awaiting_qa",
|
||||||
|
"awaiting_documentation",
|
||||||
|
}
|
||||||
|
if task.get("status") not in active_statuses:
|
||||||
return _format_error_response(
|
return _format_error_response(
|
||||||
"INVALID_STATE",
|
"INVALID_STATE",
|
||||||
"Can only update progress for in_progress tasks",
|
f"Can only update progress for active tasks. Current: {task.get('status')}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add progress update
|
# Add progress update
|
||||||
@@ -608,7 +660,7 @@ async def _handle_task_block(
|
|||||||
data: TaskBlockInput,
|
data: TaskBlockInput,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Handle task blocking."""
|
"""Handle task blocking via the soft-block endpoint."""
|
||||||
task_resp = await client.get(f"/tasks/{data.task_id}")
|
task_resp = await client.get(f"/tasks/{data.task_id}")
|
||||||
if task_resp.is_status(status.HTTP_404_NOT_FOUND):
|
if task_resp.is_status(status.HTTP_404_NOT_FOUND):
|
||||||
return _format_error_response("NOT_FOUND", f"Task {data.task_id} not found")
|
return _format_error_response("NOT_FOUND", f"Task {data.task_id} not found")
|
||||||
@@ -624,24 +676,13 @@ async def _handle_task_block(
|
|||||||
"Can only block in_progress tasks",
|
"Can only block in_progress tasks",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build blocker note for dev_notes
|
# Use the soft-block endpoint which handles status change and notes
|
||||||
blocker_note = (
|
block_resp = await client.post(
|
||||||
f"[BLOCKED - {data.blocker_type.upper()}]\n"
|
f"/tasks/{data.task_id}/soft-block",
|
||||||
f"Reason: {data.reason}\n"
|
|
||||||
f"What's needed: {data.what_needed}"
|
|
||||||
)
|
|
||||||
existing_notes = task.get("dev_notes") or ""
|
|
||||||
if existing_notes:
|
|
||||||
updated_notes = f"{existing_notes}\n\n{blocker_note}"
|
|
||||||
else:
|
|
||||||
updated_notes = blocker_note
|
|
||||||
|
|
||||||
# Block the task using PATCH to update status and notes
|
|
||||||
block_resp = await client.patch(
|
|
||||||
f"/tasks/{data.task_id}",
|
|
||||||
json={
|
json={
|
||||||
"status": "blocked",
|
"reason": data.reason,
|
||||||
"dev_notes": updated_notes,
|
"blocker_type": data.blocker_type,
|
||||||
|
"what_needed": data.what_needed,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -813,46 +854,50 @@ async def _handle_task_submit_qa(
|
|||||||
agent_id: str,
|
agent_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Handle task QA submission."""
|
"""Handle task QA submission."""
|
||||||
|
# Validate inputs
|
||||||
if not dev_notes or not handoff_summary:
|
if not dev_notes or not handoff_summary:
|
||||||
return _format_error_response(
|
return _format_error_response(
|
||||||
"MISSING_NOTES",
|
"MISSING_NOTES",
|
||||||
"Both dev_notes and handoff_summary are required for QA submission.",
|
"Both dev_notes and handoff_summary are required for QA submission.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate task exists and ownership
|
||||||
task_resp = await client.get(f"/tasks/{task_id}")
|
task_resp = await client.get(f"/tasks/{task_id}")
|
||||||
if task_resp.is_status(status.HTTP_404_NOT_FOUND):
|
if task_resp.is_status(status.HTTP_404_NOT_FOUND):
|
||||||
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
|
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
|
||||||
|
|
||||||
task = task_resp.json()
|
task = task_resp.json()
|
||||||
|
|
||||||
if error := await _validate_task_ownership(task, agent_id, client):
|
if error := await _validate_task_ownership(task, agent_id, client):
|
||||||
return error
|
return error
|
||||||
|
|
||||||
|
# Validate state
|
||||||
if task.get("status") != "verifying":
|
if task.get("status") != "verifying":
|
||||||
return _format_error_response(
|
return _format_error_response(
|
||||||
"INVALID_STATE",
|
"INVALID_STATE", "Can only submit verified tasks for QA"
|
||||||
"Can only submit verified tasks for QA",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update with notes - combine dev_notes and handoff summary
|
# Save dev notes and handoff summary, then submit for QA
|
||||||
# (handoff summary goes into dev_notes for documenter to read)
|
|
||||||
combined_notes = f"{dev_notes}\n\n---\nHandoff Summary:\n{handoff_summary}"
|
combined_notes = f"{dev_notes}\n\n---\nHandoff Summary:\n{handoff_summary}"
|
||||||
await client.patch(f"/tasks/{task_id}", json={"dev_notes": combined_notes})
|
notes_resp = await client.patch(
|
||||||
|
f"/tasks/{task_id}", json={"dev_notes": combined_notes}
|
||||||
|
)
|
||||||
|
if not notes_resp.ok:
|
||||||
|
return _format_error_response(
|
||||||
|
"NOTES_SAVE_FAILED",
|
||||||
|
"Failed to save dev notes. QA submission aborted.",
|
||||||
|
)
|
||||||
|
|
||||||
# Submit for QA
|
|
||||||
qa_resp = await client.post(f"/tasks/{task_id}/submit-qa")
|
qa_resp = await client.post(f"/tasks/{task_id}/submit-qa")
|
||||||
|
return (
|
||||||
if not qa_resp.ok:
|
_format_task_response(
|
||||||
return _format_error_response("SUBMIT_FAILED", "Failed to submit for QA")
|
qa_resp.json(),
|
||||||
|
"WAIT_FOR_QA",
|
||||||
qa_task = qa_resp.json()
|
"Task submitted for QA review.\n"
|
||||||
|
"You will be notified of the result.\n"
|
||||||
return _format_task_response(
|
"In the meantime, call roboco_task_scan for other work.",
|
||||||
qa_task,
|
)
|
||||||
"WAIT_FOR_QA",
|
if qa_resp.ok
|
||||||
"Task submitted for QA review.\n"
|
else _format_error_response("SUBMIT_FAILED", "Failed to submit for QA")
|
||||||
"You will be notified of the result.\n"
|
|
||||||
"In the meantime, call roboco_task_scan for other work.",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -863,11 +908,13 @@ async def _handle_task_qa_pass(
|
|||||||
agent_id: str,
|
agent_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Handle task QA pass."""
|
"""Handle task QA pass."""
|
||||||
# Check if agent has QA role (simple check - real impl would verify)
|
# Check if agent has QA role using canonical role lookup
|
||||||
if "qa" not in agent_id.lower():
|
agent_role = get_agent_role(agent_id)
|
||||||
|
if agent_role != "qa":
|
||||||
return _format_error_response(
|
return _format_error_response(
|
||||||
"NOT_QA",
|
"NOT_QA",
|
||||||
"Only QA agents can pass tasks through QA review.",
|
"Only QA agents can pass tasks through QA review.",
|
||||||
|
{"your_role": agent_role},
|
||||||
)
|
)
|
||||||
|
|
||||||
task_resp = await client.get(f"/tasks/{task_id}")
|
task_resp = await client.get(f"/tasks/{task_id}")
|
||||||
@@ -925,10 +972,13 @@ async def _handle_task_qa_fail(
|
|||||||
agent_id: str,
|
agent_id: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Handle task QA failure."""
|
"""Handle task QA failure."""
|
||||||
if "qa" not in agent_id.lower():
|
# Check if agent has QA role using canonical role lookup
|
||||||
|
agent_role = get_agent_role(agent_id)
|
||||||
|
if agent_role != "qa":
|
||||||
return _format_error_response(
|
return _format_error_response(
|
||||||
"NOT_QA",
|
"NOT_QA",
|
||||||
"Only QA agents can fail tasks in QA review.",
|
"Only QA agents can fail tasks in QA review.",
|
||||||
|
{"your_role": agent_role},
|
||||||
)
|
)
|
||||||
|
|
||||||
if not issues:
|
if not issues:
|
||||||
@@ -974,8 +1024,22 @@ async def _handle_task_qa_fail(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _handle_task_complete(client: ApiClient, task_id: str) -> dict[str, Any]:
|
async def _handle_docs_complete(
|
||||||
"""Handle task completion."""
|
client: ApiClient,
|
||||||
|
task_id: str,
|
||||||
|
agent_id: str,
|
||||||
|
doc_notes: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Handle documentation completion (documenter only)."""
|
||||||
|
# Check if agent is a documenter
|
||||||
|
agent_role = get_agent_role(agent_id)
|
||||||
|
if agent_role != "documenter":
|
||||||
|
return _format_error_response(
|
||||||
|
"NOT_DOCUMENTER",
|
||||||
|
"Only documenters can mark documentation as complete.",
|
||||||
|
{"your_role": agent_role},
|
||||||
|
)
|
||||||
|
|
||||||
task_resp = await client.get(f"/tasks/{task_id}")
|
task_resp = await client.get(f"/tasks/{task_id}")
|
||||||
if task_resp.is_status(status.HTTP_404_NOT_FOUND):
|
if task_resp.is_status(status.HTTP_404_NOT_FOUND):
|
||||||
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
|
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
|
||||||
@@ -985,7 +1049,56 @@ async def _handle_task_complete(client: ApiClient, task_id: str) -> dict[str, An
|
|||||||
if task.get("status") != "awaiting_documentation":
|
if task.get("status") != "awaiting_documentation":
|
||||||
return _format_error_response(
|
return _format_error_response(
|
||||||
"INVALID_STATE",
|
"INVALID_STATE",
|
||||||
"Task must be awaiting documentation to complete",
|
"Task must be awaiting documentation to mark docs complete",
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {"notes": doc_notes} if doc_notes else {}
|
||||||
|
docs_resp = await client.post(f"/tasks/{task_id}/docs-complete", json=payload)
|
||||||
|
|
||||||
|
if not docs_resp.ok:
|
||||||
|
return _format_error_response(
|
||||||
|
"DOCS_COMPLETE_FAILED",
|
||||||
|
"Failed to mark documentation complete",
|
||||||
|
{"status_code": docs_resp.status_code, "api_error": docs_resp.text},
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_task = docs_resp.json()
|
||||||
|
|
||||||
|
return _format_task_response(
|
||||||
|
updated_task,
|
||||||
|
"AWAITING_PM",
|
||||||
|
"Documentation complete! Task is now awaiting PM review.\n"
|
||||||
|
"The Cell PM will review and complete the task.\n"
|
||||||
|
"Call roboco_task_scan for next documentation task.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_task_complete(
|
||||||
|
client: ApiClient,
|
||||||
|
task_id: str,
|
||||||
|
agent_id: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Handle task completion (PM only)."""
|
||||||
|
# Check if agent can complete tasks (PM role)
|
||||||
|
if not can_cancel_tasks(agent_id): # Same roles that can cancel can complete
|
||||||
|
role = get_agent_role(agent_id)
|
||||||
|
return _format_error_response(
|
||||||
|
"NOT_PM",
|
||||||
|
"Only PMs can complete tasks after reviewing.",
|
||||||
|
{"your_role": role},
|
||||||
|
)
|
||||||
|
|
||||||
|
task_resp = await client.get(f"/tasks/{task_id}")
|
||||||
|
if task_resp.is_status(status.HTTP_404_NOT_FOUND):
|
||||||
|
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
|
||||||
|
|
||||||
|
task = task_resp.json()
|
||||||
|
|
||||||
|
if task.get("status") != "awaiting_pm_review":
|
||||||
|
return _format_error_response(
|
||||||
|
"INVALID_STATE",
|
||||||
|
"Task must be awaiting PM review to complete. "
|
||||||
|
"Documenter should call roboco_task_docs_complete first.",
|
||||||
)
|
)
|
||||||
|
|
||||||
complete_resp = await client.post(f"/tasks/{task_id}/complete")
|
complete_resp = await client.post(f"/tasks/{task_id}/complete")
|
||||||
@@ -994,10 +1107,7 @@ async def _handle_task_complete(client: ApiClient, task_id: str) -> dict[str, An
|
|||||||
return _format_error_response(
|
return _format_error_response(
|
||||||
"COMPLETE_FAILED",
|
"COMPLETE_FAILED",
|
||||||
"Failed to complete task",
|
"Failed to complete task",
|
||||||
{
|
{"status_code": complete_resp.status_code, "api_error": complete_resp.text},
|
||||||
"status_code": complete_resp.status_code,
|
|
||||||
"api_error": complete_resp.text,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
completed_task = complete_resp.json()
|
completed_task = complete_resp.json()
|
||||||
@@ -1005,7 +1115,60 @@ async def _handle_task_complete(client: ApiClient, task_id: str) -> dict[str, An
|
|||||||
return _format_task_response(
|
return _format_task_response(
|
||||||
completed_task,
|
completed_task,
|
||||||
"DONE",
|
"DONE",
|
||||||
"Task completed successfully!\nCall roboco_task_scan for new work.",
|
"Task completed successfully!\nCall roboco_task_scan for more work.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_task_cancel(
|
||||||
|
client: ApiClient,
|
||||||
|
task_id: str,
|
||||||
|
agent_id: str,
|
||||||
|
reason: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Handle task cancellation (PM and board only)."""
|
||||||
|
# Check permission first
|
||||||
|
if not can_cancel_tasks(agent_id):
|
||||||
|
role = get_agent_role(agent_id)
|
||||||
|
return _format_error_response(
|
||||||
|
"NOT_AUTHORIZED",
|
||||||
|
"Only PMs and board members can cancel tasks",
|
||||||
|
{"your_role": role},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get task to verify it exists
|
||||||
|
task_resp = await client.get(f"/tasks/{task_id}")
|
||||||
|
if not task_resp.ok:
|
||||||
|
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
|
||||||
|
|
||||||
|
task = task_resp.json()
|
||||||
|
current_status = task.get("status")
|
||||||
|
|
||||||
|
# Terminal states can't be cancelled
|
||||||
|
if current_status in ("completed", "cancelled"):
|
||||||
|
return _format_error_response(
|
||||||
|
"INVALID_STATE",
|
||||||
|
f"Cannot cancel task in '{current_status}' status",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cancel the task
|
||||||
|
cancel_resp = await client.post(f"/tasks/{task_id}/cancel")
|
||||||
|
|
||||||
|
if not cancel_resp.ok:
|
||||||
|
return _format_error_response(
|
||||||
|
"CANCEL_FAILED",
|
||||||
|
"Failed to cancel task",
|
||||||
|
{
|
||||||
|
"status_code": cancel_resp.status_code,
|
||||||
|
"api_error": cancel_resp.text,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
cancelled_task = cancel_resp.json()
|
||||||
|
|
||||||
|
return _format_task_response(
|
||||||
|
cancelled_task,
|
||||||
|
"CANCELLED",
|
||||||
|
f"Task cancelled.{' Reason: ' + reason if reason else ''}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1498,15 +1661,19 @@ def create_task_mcp_server(agent_id: str) -> FastMCP:
|
|||||||
async def roboco_task_progress(
|
async def roboco_task_progress(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
message: str,
|
message: str,
|
||||||
percentage: int | None = None,
|
percentage: int,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Update task progress.
|
Update task progress.
|
||||||
|
|
||||||
|
ENFORCEMENT:
|
||||||
|
- Percentage is REQUIRED (0-100) to show real progress
|
||||||
|
- Message must describe what was accomplished
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task_id: The task UUID
|
task_id: The task UUID
|
||||||
message: Progress update message
|
message: Progress update message describing work done
|
||||||
percentage: Optional completion percentage (0-100)
|
percentage: Completion percentage (0-100), required
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Updated task
|
Updated task
|
||||||
@@ -1685,13 +1852,41 @@ def create_task_mcp_server(agent_id: str) -> FastMCP:
|
|||||||
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)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_task_complete(task_id: str) -> dict[str, Any]:
|
async def roboco_task_docs_complete(
|
||||||
|
task_id: str,
|
||||||
|
doc_notes: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Mark task as completed (typically by Documenter).
|
Mark documentation as complete (documenter only).
|
||||||
|
|
||||||
|
Transitions task from awaiting_documentation to awaiting_pm_review.
|
||||||
|
The Cell PM will then review and complete the task.
|
||||||
|
|
||||||
ENFORCEMENT:
|
ENFORCEMENT:
|
||||||
|
- Only documenters can use this tool
|
||||||
- Task must be in 'awaiting_documentation' status
|
- Task must be in 'awaiting_documentation' status
|
||||||
- Documentation must exist
|
- Cannot document your own task (self-review prevention)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: The task UUID
|
||||||
|
doc_notes: Optional notes about the documentation completed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Task now awaiting PM review
|
||||||
|
"""
|
||||||
|
return await _handle_docs_complete(client, task_id, agent_id, doc_notes)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def roboco_task_complete(task_id: str) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Mark task as completed (PM only).
|
||||||
|
|
||||||
|
Only PMs can complete tasks, after documenter marks docs complete.
|
||||||
|
This is the final step in the workflow: Dev → QA → Documenter → PM.
|
||||||
|
|
||||||
|
ENFORCEMENT:
|
||||||
|
- Only PMs can use this tool
|
||||||
|
- Task must be in 'awaiting_pm_review' status
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task_id: The task UUID
|
task_id: The task UUID
|
||||||
@@ -1699,7 +1894,7 @@ def create_task_mcp_server(agent_id: str) -> FastMCP:
|
|||||||
Returns:
|
Returns:
|
||||||
Completed task
|
Completed task
|
||||||
"""
|
"""
|
||||||
return await _handle_task_complete(client, task_id)
|
return await _handle_task_complete(client, task_id, agent_id)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_agent_idle() -> dict[str, Any]:
|
async def roboco_agent_idle() -> dict[str, Any]:
|
||||||
@@ -1770,6 +1965,33 @@ def create_task_mcp_server(agent_id: str) -> FastMCP:
|
|||||||
input_data = TaskAssignInput(task_id=task_id, assignee=assignee)
|
input_data = TaskAssignInput(task_id=task_id, assignee=assignee)
|
||||||
return await _handle_task_assign(client, input_data, agent_id)
|
return await _handle_task_assign(client, input_data, agent_id)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def roboco_task_cancel(
|
||||||
|
task_id: str,
|
||||||
|
reason: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Cancel a task (PM and board only).
|
||||||
|
|
||||||
|
Use this to:
|
||||||
|
- Cancel obsolete or duplicate tasks
|
||||||
|
- Cancel tasks that are no longer needed
|
||||||
|
- Cancel blocked tasks that cannot be resolved
|
||||||
|
|
||||||
|
ENFORCEMENT:
|
||||||
|
- Only PMs and board members can cancel tasks
|
||||||
|
- CEO and Auditor cannot cancel (they observe only)
|
||||||
|
- Cannot cancel completed or already-cancelled tasks
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: The task UUID to cancel
|
||||||
|
reason: Optional reason for cancellation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cancelled task confirmation
|
||||||
|
"""
|
||||||
|
return await _handle_task_cancel(client, task_id, agent_id, reason)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_task_escalate(
|
async def roboco_task_escalate(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
|
|||||||
+2
-2
@@ -97,11 +97,11 @@ async def resolve_agent_uuid(
|
|||||||
if len(agent_id) == _UUID_LENGTH and agent_id.count("-") == _UUID_HYPHEN_COUNT:
|
if len(agent_id) == _UUID_LENGTH and agent_id.count("-") == _UUID_HYPHEN_COUNT:
|
||||||
return agent_id
|
return agent_id
|
||||||
|
|
||||||
# Look up by slug
|
# Look up by slug - GET /agents/{id} accepts both UUID and slug
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
resp = await client.get(
|
resp = await client.get(
|
||||||
f"{settings.internal_api_url}/agents/by-slug/{agent_id}",
|
f"{settings.internal_api_url}/agents/{agent_id}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
if resp.status_code == _HTTP_OK:
|
if resp.status_code == _HTTP_OK:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class TaskStatus(str, Enum):
|
|||||||
NEEDS_REVISION = "needs_revision"
|
NEEDS_REVISION = "needs_revision"
|
||||||
AWAITING_QA = "awaiting_qa"
|
AWAITING_QA = "awaiting_qa"
|
||||||
AWAITING_DOCUMENTATION = "awaiting_documentation"
|
AWAITING_DOCUMENTATION = "awaiting_documentation"
|
||||||
|
AWAITING_PM_REVIEW = "awaiting_pm_review" # After docs, before PM completes
|
||||||
COMPLETED = "completed"
|
COMPLETED = "completed"
|
||||||
CANCELLED = "cancelled"
|
CANCELLED = "cancelled"
|
||||||
|
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ class WaitingRecord:
|
|||||||
# Model mapping for cost optimization
|
# Model mapping for cost optimization
|
||||||
MODEL_MAP: dict[str, str] = {
|
MODEL_MAP: dict[str, str] = {
|
||||||
"sonnet": "claude-sonnet-4-20250514",
|
"sonnet": "claude-sonnet-4-20250514",
|
||||||
"opus": "claude-opus-4-20250514",
|
"opus": "claude-opus-4-5-20251101",
|
||||||
"haiku": "claude-haiku-4-20250514",
|
"haiku": "claude-haiku-4-5-20251001",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import httpx
|
|||||||
import structlog
|
import structlog
|
||||||
from fastapi import status as http_status
|
from fastapi import status as http_status
|
||||||
|
|
||||||
|
from roboco.agents_config import get_agent_role
|
||||||
from roboco.config import settings
|
from roboco.config import settings
|
||||||
from roboco.models.runtime import (
|
from roboco.models.runtime import (
|
||||||
MODEL_MAP,
|
MODEL_MAP,
|
||||||
@@ -200,23 +201,18 @@ class AgentOrchestrator:
|
|||||||
# Journal - always needed for reflection
|
# Journal - always needed for reflection
|
||||||
"mcp__roboco-journal__*",
|
"mcp__roboco-journal__*",
|
||||||
# File operations for documenters and developers
|
# File operations for documenters and developers
|
||||||
"Write(path:/app/docs/**)",
|
# Note: // prefix = absolute path (container paths like /app/docs)
|
||||||
"Write(path:/app/CHANGELOG.md)",
|
"Write(//app/docs/**)",
|
||||||
"Write(path:/app/README.md)",
|
"Write(//app/CHANGELOG.md)",
|
||||||
"Edit(path:/app/docs/**)",
|
"Write(//app/README.md)",
|
||||||
"Edit(path:/app/CHANGELOG.md)",
|
"Edit(//app/docs/**)",
|
||||||
"Edit(path:/app/README.md)",
|
"Edit(//app/CHANGELOG.md)",
|
||||||
|
"Edit(//app/README.md)",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Path to agent Claude settings (shared across all agents)
|
# Path to agent Claude settings (shared across all agents)
|
||||||
# When running in container: Claude auth is mounted to /root/.claude
|
# Always use CLAUDE_AUTH_HOST_PATH - agents mount from this location
|
||||||
# When running on host: use CLAUDE_AUTH_HOST_PATH directly
|
claude_dir = Path(CLAUDE_AUTH_HOST_PATH)
|
||||||
if PROJECT_HOST_PATH:
|
|
||||||
# Running in container - use the mounted path
|
|
||||||
claude_dir = Path("/root/.claude")
|
|
||||||
else:
|
|
||||||
# Running on host
|
|
||||||
claude_dir = Path(CLAUDE_AUTH_HOST_PATH)
|
|
||||||
|
|
||||||
settings_path = claude_dir / "settings.json"
|
settings_path = claude_dir / "settings.json"
|
||||||
|
|
||||||
@@ -298,10 +294,10 @@ class AgentOrchestrator:
|
|||||||
# Generate MCP config
|
# Generate MCP config
|
||||||
mcp_config_path = await self._generate_mcp_config(agent_id)
|
mcp_config_path = await self._generate_mcp_config(agent_id)
|
||||||
|
|
||||||
# Determine model
|
# Determine model using canonical role name from agents_config
|
||||||
if not model:
|
if not model:
|
||||||
role = self._get_agent_role(agent_id)
|
canonical_role = get_agent_role(agent_id)
|
||||||
model = ROLE_MODEL_MAP.get(role, "sonnet")
|
model = ROLE_MODEL_MAP.get(canonical_role, "sonnet")
|
||||||
|
|
||||||
# Create config
|
# Create config
|
||||||
config = AgentConfig(
|
config = AgentConfig(
|
||||||
@@ -540,7 +536,7 @@ class AgentOrchestrator:
|
|||||||
|
|
||||||
def _get_blueprint_path(self, agent_id: str) -> Path:
|
def _get_blueprint_path(self, agent_id: str) -> Path:
|
||||||
"""Get blueprint path for an agent."""
|
"""Get blueprint path for an agent."""
|
||||||
role = self._get_agent_role(agent_id)
|
role = self._get_blueprint_role(agent_id)
|
||||||
team = self._get_agent_team(agent_id)
|
team = self._get_agent_team(agent_id)
|
||||||
|
|
||||||
if team == "backend":
|
if team == "backend":
|
||||||
@@ -557,7 +553,7 @@ class AgentOrchestrator:
|
|||||||
|
|
||||||
def _get_blueprint_rel_path(self, agent_id: str) -> str:
|
def _get_blueprint_rel_path(self, agent_id: str) -> str:
|
||||||
"""Get relative blueprint path for container mount."""
|
"""Get relative blueprint path for container mount."""
|
||||||
role = self._get_agent_role(agent_id)
|
role = self._get_blueprint_role(agent_id)
|
||||||
team = self._get_agent_team(agent_id)
|
team = self._get_agent_team(agent_id)
|
||||||
|
|
||||||
if team == "backend":
|
if team == "backend":
|
||||||
@@ -572,8 +568,8 @@ class AgentOrchestrator:
|
|||||||
blueprint_file = f"{role.replace('_', '-')}.md"
|
blueprint_file = f"{role.replace('_', '-')}.md"
|
||||||
return f"{cell_dir}/{blueprint_file}"
|
return f"{cell_dir}/{blueprint_file}"
|
||||||
|
|
||||||
def _get_agent_role(self, agent_id: str) -> str:
|
def _get_blueprint_role(self, agent_id: str) -> str:
|
||||||
"""Get role from agent_id."""
|
"""Get blueprint-specific role name from agent_id (used for file paths)."""
|
||||||
role_map = {
|
role_map = {
|
||||||
"be-dev-1": "be-dev",
|
"be-dev-1": "be-dev",
|
||||||
"be-dev-2": "be-dev",
|
"be-dev-2": "be-dev",
|
||||||
@@ -1298,6 +1294,7 @@ Start now: roboco_task_get("{task_id}")
|
|||||||
await self._dispatch_dev_work(client)
|
await self._dispatch_dev_work(client)
|
||||||
await self._dispatch_qa_work(client)
|
await self._dispatch_qa_work(client)
|
||||||
await self._dispatch_doc_work(client)
|
await self._dispatch_doc_work(client)
|
||||||
|
await self._dispatch_pm_review_work(client)
|
||||||
await self._dispatch_marketing_work(client)
|
await self._dispatch_marketing_work(client)
|
||||||
|
|
||||||
# Event-based dispatchers (check blockers, notifications)
|
# Event-based dispatchers (check blockers, notifications)
|
||||||
@@ -1599,6 +1596,32 @@ Begin with step 1: roboco_task_get("{task_id}")
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
async def _dispatch_pm_review_work(self, client: httpx.AsyncClient) -> None:
|
||||||
|
"""
|
||||||
|
Dispatch PM review work to cell PMs.
|
||||||
|
|
||||||
|
Monitors: awaiting_pm_review tasks
|
||||||
|
Spawns: be-pm, fe-pm, ux-pm
|
||||||
|
"""
|
||||||
|
tasks = await self._fetch_tasks(client, "awaiting_pm_review")
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
team = task.get("team")
|
||||||
|
if team not in ["backend", "frontend", "ux_ui"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pm_id = self._TEAM_PM_MAP.get(team, "be-pm")
|
||||||
|
|
||||||
|
if self._is_agent_active(pm_id):
|
||||||
|
continue
|
||||||
|
|
||||||
|
await self.spawn_agent(
|
||||||
|
agent_id=pm_id,
|
||||||
|
task_id=task["id"],
|
||||||
|
initial_prompt=self._build_pm_review_prompt(task),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
async def _dispatch_marketing_work(self, client: httpx.AsyncClient) -> None:
|
async def _dispatch_marketing_work(self, client: httpx.AsyncClient) -> None:
|
||||||
"""
|
"""
|
||||||
Dispatch marketing work to head-marketing.
|
Dispatch marketing work to head-marketing.
|
||||||
@@ -1808,9 +1831,31 @@ Begin documentation:
|
|||||||
1. Call roboco_task_get("{task_id}") for full details and dev handoff notes
|
1. Call roboco_task_get("{task_id}") for full details and dev handoff notes
|
||||||
2. Create or update documentation based on what was implemented
|
2. Create or update documentation based on what was implemented
|
||||||
3. Ensure code comments, README updates, API docs as needed
|
3. Ensure code comments, README updates, API docs as needed
|
||||||
4. Call roboco_task_complete("{task_id}") when documentation is done
|
4. Call roboco_task_docs_complete("{task_id}") when documentation is done
|
||||||
5. Call roboco_task_scan() to check for more documentation work
|
5. Call roboco_task_scan() to check for more documentation work
|
||||||
6. If no more work, call roboco_agent_idle() to shutdown gracefully
|
6. If no more work, call roboco_agent_idle() to shutdown gracefully
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _build_pm_review_prompt(self, task: dict[str, Any]) -> str:
|
||||||
|
"""Build initial prompt for PM to review and complete a task."""
|
||||||
|
task_id = task.get("id", "unknown")
|
||||||
|
title = task.get("title", "Untitled")
|
||||||
|
team = task.get("team", "unknown")
|
||||||
|
|
||||||
|
return f"""A task is awaiting your PM review for final completion.
|
||||||
|
|
||||||
|
TASK ID: {task_id}
|
||||||
|
TITLE: {title}
|
||||||
|
TEAM: {team}
|
||||||
|
|
||||||
|
This task has passed QA and documentation. Review and complete:
|
||||||
|
|
||||||
|
1. Call roboco_task_get("{task_id}") to review the task details
|
||||||
|
2. Verify dev_notes, QA notes, and documentation are satisfactory
|
||||||
|
3. If this task has subtasks, verify all subtasks are completed
|
||||||
|
4. Call roboco_task_complete("{task_id}") to finalize the task
|
||||||
|
5. Call roboco_task_scan() to check for more tasks needing review
|
||||||
|
6. If no more work, call roboco_agent_idle() to shutdown gracefully
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _build_marketing_prompt(self, task: dict[str, Any]) -> str:
|
def _build_marketing_prompt(self, task: dict[str, Any]) -> str:
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from roboco.models.journal import (
|
|||||||
create_struggle_entry,
|
create_struggle_entry,
|
||||||
create_task_reflection,
|
create_task_reflection,
|
||||||
)
|
)
|
||||||
|
from roboco.models.optimal import IndexJournalEntryParams
|
||||||
from roboco.utils.converters import require_uuid, to_python_uuid
|
from roboco.utils.converters import require_uuid, to_python_uuid
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
logger = structlog.get_logger()
|
||||||
@@ -242,14 +243,16 @@ class JournalService:
|
|||||||
try:
|
try:
|
||||||
optimal = await self._get_optimal_service()
|
optimal = await self._get_optimal_service()
|
||||||
await optimal.index_journal_entry(
|
await optimal.index_journal_entry(
|
||||||
entry_id=entry_row.id,
|
IndexJournalEntryParams(
|
||||||
agent_id=journal_row.agent_id
|
entry_id=entry_row.id,
|
||||||
if journal_row
|
agent_id=journal_row.agent_id
|
||||||
else entry_create.journal_id,
|
if journal_row
|
||||||
content=entry_create.content,
|
else entry_create.journal_id,
|
||||||
entry_type=type_key,
|
content=entry_create.content,
|
||||||
task_id=entry_create.task_id,
|
entry_type=type_key,
|
||||||
tags=entry_create.tags,
|
task_id=entry_create.task_id,
|
||||||
|
tags=entry_create.tags,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Failed to index journal entry in RAG", error=str(e))
|
logger.warning("Failed to index journal entry in RAG", error=str(e))
|
||||||
|
|||||||
+202
-43
@@ -15,10 +15,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from roboco.db.tables import AgentTable, TaskTable
|
from roboco.db.tables import AgentTable, TaskTable
|
||||||
from roboco.enforcement import (
|
from roboco.enforcement import (
|
||||||
TaskLifecycleError,
|
|
||||||
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
|
||||||
@@ -180,19 +178,24 @@ class TaskService:
|
|||||||
)
|
)
|
||||||
agent = agent_result.scalar_one_or_none()
|
agent = agent_result.scalar_one_or_none()
|
||||||
|
|
||||||
# Base valid statuses for claiming
|
# Role-based claiming: each role can only claim specific statuses
|
||||||
valid_statuses = {TaskStatus.PENDING}
|
# QA → awaiting_qa only, Documenter → awaiting_documentation only
|
||||||
if allow_reassign:
|
# Developers/PMs → pending (and claimed if allow_reassign)
|
||||||
valid_statuses.add(TaskStatus.CLAIMED)
|
valid_statuses: set[TaskStatus] = set()
|
||||||
|
|
||||||
# Role-based claiming: QA and Documenters can claim specific statuses
|
|
||||||
# If role is missing but task requires specific role, reject the claim
|
|
||||||
if agent and agent.role:
|
if agent and agent.role:
|
||||||
role = agent.role.value if hasattr(agent.role, "value") else str(agent.role)
|
role = agent.role.value if hasattr(agent.role, "value") else str(agent.role)
|
||||||
if role == "qa":
|
if role == "qa":
|
||||||
|
# QA can ONLY claim awaiting_qa tasks
|
||||||
valid_statuses.add(TaskStatus.AWAITING_QA)
|
valid_statuses.add(TaskStatus.AWAITING_QA)
|
||||||
elif role == "documenter":
|
elif role == "documenter":
|
||||||
|
# Documenter can ONLY claim awaiting_documentation tasks
|
||||||
valid_statuses.add(TaskStatus.AWAITING_DOCUMENTATION)
|
valid_statuses.add(TaskStatus.AWAITING_DOCUMENTATION)
|
||||||
|
else:
|
||||||
|
# Developer, PM, and other roles claim pending tasks
|
||||||
|
valid_statuses.add(TaskStatus.PENDING)
|
||||||
|
if allow_reassign:
|
||||||
|
valid_statuses.add(TaskStatus.CLAIMED)
|
||||||
elif task.status in {TaskStatus.AWAITING_QA, TaskStatus.AWAITING_DOCUMENTATION}:
|
elif task.status in {TaskStatus.AWAITING_QA, TaskStatus.AWAITING_DOCUMENTATION}:
|
||||||
# No role information - reject claims for role-specific statuses
|
# No role information - reject claims for role-specific statuses
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -204,6 +207,11 @@ class TaskService:
|
|||||||
agent_role="none",
|
agent_role="none",
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
else:
|
||||||
|
# No role info but task is pending - allow claim (fallback)
|
||||||
|
valid_statuses.add(TaskStatus.PENDING)
|
||||||
|
if allow_reassign:
|
||||||
|
valid_statuses.add(TaskStatus.CLAIMED)
|
||||||
|
|
||||||
if task.status not in valid_statuses:
|
if task.status not in valid_statuses:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -225,15 +233,18 @@ class TaskService:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# For QA/Documenter claiming, store previous owner for self-review checks
|
# For QA/Documenter claiming, ensure original_developer is set for
|
||||||
# before changing assigned_to
|
# self-review checks. Primary storage is in submit_for_qa, but we set
|
||||||
|
# here as fallback (e.g., if task was created directly in awaiting_qa)
|
||||||
if agent and agent.role:
|
if agent and agent.role:
|
||||||
role = agent.role.value if hasattr(agent.role, "value") else str(agent.role)
|
role = agent.role.value if hasattr(agent.role, "value") else str(agent.role)
|
||||||
if role in ("qa", "documenter"):
|
if role in ("qa", "documenter"):
|
||||||
# Store original developer in quick_context for self-review check
|
# Only set if not already stored by submit_for_qa
|
||||||
original_dev = str(task.assigned_to) if task.assigned_to else None
|
existing_context = task.quick_context or ""
|
||||||
if original_dev:
|
if "original_developer:" not in existing_context:
|
||||||
task.quick_context = f"original_developer:{original_dev}"
|
original_dev = str(task.assigned_to) if task.assigned_to else None
|
||||||
|
if original_dev:
|
||||||
|
task.quick_context = f"original_developer:{original_dev}"
|
||||||
|
|
||||||
# All roles: update assigned_to and claimed_at
|
# All roles: update assigned_to and claimed_at
|
||||||
task.assigned_to = cast("Any", agent_id)
|
task.assigned_to = cast("Any", agent_id)
|
||||||
@@ -290,7 +301,16 @@ class TaskService:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if task.status not in (TaskStatus.CLAIMED, TaskStatus.PAUSED):
|
# Valid statuses to start/resume work:
|
||||||
|
# - CLAIMED: Developer just claimed a pending task
|
||||||
|
# - PAUSED: Developer resuming paused work
|
||||||
|
# - NEEDS_REVISION: Developer resuming after QA rejection
|
||||||
|
valid_start_statuses = (
|
||||||
|
TaskStatus.CLAIMED,
|
||||||
|
TaskStatus.PAUSED,
|
||||||
|
TaskStatus.NEEDS_REVISION,
|
||||||
|
)
|
||||||
|
if task.status not in valid_start_statuses:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Cannot start task - invalid status",
|
"Cannot start task - invalid status",
|
||||||
task_id=str(task_id),
|
task_id=str(task_id),
|
||||||
@@ -298,7 +318,9 @@ class TaskService:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
task.started_at = datetime.now(UTC)
|
# Only update started_at if this is the first time starting
|
||||||
|
if task.started_at is None:
|
||||||
|
task.started_at = datetime.now(UTC)
|
||||||
task.status = TaskStatus.IN_PROGRESS
|
task.status = TaskStatus.IN_PROGRESS
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
|
|
||||||
@@ -330,6 +352,61 @@ class TaskService:
|
|||||||
)
|
)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
|
async def soft_block(
|
||||||
|
self,
|
||||||
|
task_id: UUID,
|
||||||
|
reason: str,
|
||||||
|
blocker_type: str,
|
||||||
|
what_needed: str,
|
||||||
|
) -> TaskTable | None:
|
||||||
|
"""
|
||||||
|
Block a task due to an external factor (not a task dependency).
|
||||||
|
|
||||||
|
Unlike `block()` which requires another task as the blocker,
|
||||||
|
this method handles soft blocks like:
|
||||||
|
- External dependencies (waiting for API access, credentials)
|
||||||
|
- Questions that need PM/stakeholder input
|
||||||
|
- Technical blockers (infrastructure issues)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: The task to block
|
||||||
|
reason: Why the task is blocked
|
||||||
|
blocker_type: Type of blocker (external/internal/question/dependency)
|
||||||
|
what_needed: What is needed to unblock
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The blocked task, or None if blocking not allowed
|
||||||
|
"""
|
||||||
|
task = await self.get(task_id)
|
||||||
|
if not task:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if task.status != TaskStatus.IN_PROGRESS:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Build blocker note for dev_notes
|
||||||
|
blocker_note = (
|
||||||
|
f"[BLOCKED - {blocker_type.upper()}]\n"
|
||||||
|
f"Reason: {reason}\n"
|
||||||
|
f"What's needed: {what_needed}"
|
||||||
|
)
|
||||||
|
existing_notes = task.dev_notes or ""
|
||||||
|
if existing_notes:
|
||||||
|
task.dev_notes = f"{existing_notes}\n\n{blocker_note}"
|
||||||
|
else:
|
||||||
|
task.dev_notes = blocker_note
|
||||||
|
|
||||||
|
task.status = TaskStatus.BLOCKED
|
||||||
|
await self.session.flush()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Task soft-blocked",
|
||||||
|
task_id=str(task_id),
|
||||||
|
blocker_type=blocker_type,
|
||||||
|
reason=reason,
|
||||||
|
)
|
||||||
|
return task
|
||||||
|
|
||||||
async def unblock(self, task_id: UUID) -> TaskTable | None:
|
async def unblock(self, task_id: UUID) -> TaskTable | None:
|
||||||
"""Unblock a task and resume to in_progress."""
|
"""Unblock a task and resume to in_progress."""
|
||||||
task = await self.get(task_id)
|
task = await self.get(task_id)
|
||||||
@@ -399,11 +476,22 @@ class TaskService:
|
|||||||
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
|
||||||
|
# for self-review prevention. Storing here ensures we capture the developer
|
||||||
|
# even if the task is reassigned before QA claims it.
|
||||||
|
original_dev = str(task.assigned_to) if task.assigned_to else None
|
||||||
|
if original_dev:
|
||||||
|
task.quick_context = f"original_developer:{original_dev}"
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
logger.info("Task submitted for QA", task_id=str(task_id))
|
logger.info(
|
||||||
|
"Task submitted for QA",
|
||||||
|
task_id=str(task_id),
|
||||||
|
original_developer=original_dev,
|
||||||
|
)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
async def pass_qa(
|
async def pass_qa(
|
||||||
@@ -427,7 +515,13 @@ class TaskService:
|
|||||||
return task
|
return task
|
||||||
|
|
||||||
async def fail_qa(self, task_id: UUID, notes: str) -> TaskTable | None:
|
async def fail_qa(self, task_id: UUID, notes: str) -> TaskTable | None:
|
||||||
"""Mark task as failed QA."""
|
"""
|
||||||
|
Mark task as failed QA and reassign to original developer.
|
||||||
|
|
||||||
|
When QA fails a task, it goes back to the original developer for revision.
|
||||||
|
The original developer is extracted from quick_context which stores
|
||||||
|
"original_developer:{uuid}" when the task was submitted to QA.
|
||||||
|
"""
|
||||||
task = await self.get(task_id)
|
task = await self.get(task_id)
|
||||||
if not task:
|
if not task:
|
||||||
return None
|
return None
|
||||||
@@ -438,25 +532,105 @@ class TaskService:
|
|||||||
task.qa_notes = notes
|
task.qa_notes = notes
|
||||||
task.qa_verified = False
|
task.qa_verified = False
|
||||||
task.status = TaskStatus.NEEDS_REVISION
|
task.status = TaskStatus.NEEDS_REVISION
|
||||||
|
|
||||||
|
# Reassign to original developer so they can work on revisions
|
||||||
|
original_dev = extract_original_developer(task.quick_context)
|
||||||
|
if original_dev:
|
||||||
|
task.assigned_to = cast("Any", UUID(original_dev))
|
||||||
|
logger.info(
|
||||||
|
"Task reassigned to original developer for revision",
|
||||||
|
task_id=str(task_id),
|
||||||
|
original_developer=original_dev,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# If no original developer found, unassign so it can be claimed
|
||||||
|
task.assigned_to = None
|
||||||
|
logger.warning(
|
||||||
|
"No original developer found, task unassigned",
|
||||||
|
task_id=str(task_id),
|
||||||
|
)
|
||||||
|
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
|
|
||||||
logger.info("Task failed QA", task_id=str(task_id))
|
logger.info("Task failed QA", task_id=str(task_id))
|
||||||
return task
|
return task
|
||||||
|
|
||||||
|
async def docs_complete(
|
||||||
|
self,
|
||||||
|
task_id: UUID,
|
||||||
|
doc_notes: str | None = None,
|
||||||
|
) -> TaskTable | None:
|
||||||
|
"""
|
||||||
|
Mark documentation as complete (documenter only).
|
||||||
|
|
||||||
|
Transitions task from AWAITING_DOCUMENTATION to AWAITING_PM_REVIEW.
|
||||||
|
The Cell PM will then review and call complete() to finish the task.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: The task to mark docs complete
|
||||||
|
doc_notes: Optional notes about the documentation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated task or None if not allowed
|
||||||
|
"""
|
||||||
|
task = await self.get(task_id)
|
||||||
|
if not task:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if task.status != TaskStatus.AWAITING_DOCUMENTATION:
|
||||||
|
logger.warning(
|
||||||
|
"Cannot mark docs complete - not awaiting documentation",
|
||||||
|
task_id=str(task_id),
|
||||||
|
current_status=task.status.value,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Store doc notes in quick_context (no dedicated field for doc_notes)
|
||||||
|
if doc_notes:
|
||||||
|
existing_context = task.quick_context or ""
|
||||||
|
doc_note_entry = f"doc_notes:{doc_notes}"
|
||||||
|
task.quick_context = (
|
||||||
|
f"{existing_context}\n{doc_note_entry}".strip()
|
||||||
|
if existing_context
|
||||||
|
else doc_note_entry
|
||||||
|
)
|
||||||
|
|
||||||
|
task.status = TaskStatus.AWAITING_PM_REVIEW
|
||||||
|
|
||||||
|
# Reassign to the cell PM for final review
|
||||||
|
# Store documenter in quick_context for reference
|
||||||
|
if task.assigned_to:
|
||||||
|
existing_context = task.quick_context or ""
|
||||||
|
if "documenter:" not in existing_context:
|
||||||
|
doc_context = f"documenter:{task.assigned_to}"
|
||||||
|
task.quick_context = (
|
||||||
|
f"{existing_context}\n{doc_context}".strip()
|
||||||
|
if existing_context
|
||||||
|
else doc_context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Note: We don't auto-assign to PM here - PM will pick it up via scan
|
||||||
|
# The task remains assigned to documenter until PM claims it
|
||||||
|
await self.session.flush()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Documentation complete, awaiting PM review",
|
||||||
|
task_id=str(task_id),
|
||||||
|
)
|
||||||
|
return task
|
||||||
|
|
||||||
async def complete(
|
async def complete(
|
||||||
self,
|
self,
|
||||||
task_id: UUID,
|
task_id: UUID,
|
||||||
skip_handoff_check: bool = False,
|
|
||||||
) -> TaskTable | None:
|
) -> TaskTable | None:
|
||||||
"""
|
"""
|
||||||
Mark task as completed.
|
Mark task as completed (PM only).
|
||||||
|
|
||||||
Enforces handoff requirement: tasks in AWAITING_DOCUMENTATION
|
Only PMs can complete tasks, and only from AWAITING_PM_REVIEW status.
|
||||||
must have an accepted handoff before completion.
|
This ensures the full workflow: Dev → QA → Documenter → PM.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task_id: The task to complete
|
task_id: The task to complete
|
||||||
skip_handoff_check: Skip handoff requirement (for small tasks)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The completed task or None if completion not allowed
|
The completed task or None if completion not allowed
|
||||||
@@ -465,28 +639,13 @@ class TaskService:
|
|||||||
if not task:
|
if not task:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Validate transition using enforcement layer
|
# Only allow completion from AWAITING_PM_REVIEW
|
||||||
try:
|
# This enforces the workflow: documenter calls docs_complete, PM calls complete
|
||||||
validate_task_transition(task.status.value, TaskStatus.COMPLETED.value)
|
if task.status != TaskStatus.AWAITING_PM_REVIEW:
|
||||||
except TaskLifecycleError:
|
|
||||||
# Allow from specific states
|
|
||||||
if task.status not in (
|
|
||||||
TaskStatus.AWAITING_DOCUMENTATION,
|
|
||||||
TaskStatus.AWAITING_QA, # Small tasks may skip docs
|
|
||||||
TaskStatus.VERIFYING, # Solo dev may skip QA
|
|
||||||
):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Enforce lifecycle: tasks awaiting documentation must have passed QA
|
|
||||||
if (
|
|
||||||
task.status == TaskStatus.AWAITING_DOCUMENTATION
|
|
||||||
and not skip_handoff_check
|
|
||||||
and not task.qa_verified
|
|
||||||
):
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Cannot complete task - QA verification required",
|
"Cannot complete task - must be in awaiting_pm_review status",
|
||||||
task_id=str(task_id),
|
task_id=str(task_id),
|
||||||
status=task.status.value,
|
current_status=task.status.value,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user