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:
Renn F
2025-12-21 20:11:29 +01:00
parent 5d71f2fe9d
commit dfa8077484
28 changed files with 1441 additions and 410 deletions
+260
View File
@@ -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 |
+-------------------+----------------------------------+----------------------------------+
```
+28 -11
View File
@@ -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_start(task_id)` - Begin work (moves to in_progress)
- `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_unblock(task_id)` - Resume from blocked state
- `roboco_task_pause(task_id, reason, checkpoint_summary, remaining_work)` - Pause with checkpoint
@@ -125,7 +125,7 @@ Log your implementation decision:
### 6. EXECUTE
Work through your plan:
- **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
- Journal learnings: `roboco_journal_learning(data)`
- Journal struggles: `roboco_journal_struggle(data)`
@@ -164,15 +164,28 @@ roboco_task_pause(task_id, {
- All checks MUST pass before proceeding
### 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)`
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
roboco_task_submit_qa(task_id, {
"dev_notes": "Used Redis sliding window. Key gotcha: connection pooling required.",
"handoff_summary": "Rate limit decorator in auth/ratelimit.py. 12 new tests added."
"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. 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
{
"task_id": "{task_id}",
@@ -184,10 +197,14 @@ roboco_task_submit_qa(task_id, {
}
```
### 9. CLOSE
- After QA approval + Documentation complete
- Task transitions to "completed" automatically
- Return to SCAN: `roboco_task_scan()` or `roboco_agent_idle()`
### 9. DONE
After you submit for QA, the task flows through:
1. **QA** reviews and passes/fails
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
@@ -295,9 +312,9 @@ roboco_journal_decision({
})
# 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 ...
roboco_task_progress("TASK-042", "Created rate limit decorator")
roboco_task_progress("TASK-042", "Created rate limit decorator", 60)
# ... do more work ...
roboco_journal_learning({
+12 -8
View File
@@ -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_claim(task_id)` - Claim for documentation
- `roboco_task_start(task_id)` - Begin documentation work
- `roboco_task_progress(task_id, message)` - Update progress
- `roboco_task_complete(task_id)` - Mark documentation complete
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
- `roboco_task_docs_complete(task_id, doc_notes?)` - Mark docs done (goes to PM review)
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
**Journal:**
@@ -106,11 +106,15 @@ If none: `roboco_agent_idle()`
- {Description}
```
Update progress: `roboco_task_progress(task_id, "Completed API docs...")`
Update progress: `roboco_task_progress(task_id, "Completed API docs...", 50)`
### 7. COMPLETE
`roboco_task_complete(task_id)` - Mark task as completed
`roboco_message_send(data)` - Announce completion in #backend-cell
### 7. SUBMIT TO PM
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
This sends the task to the Cell PM for final review and completion.
`roboco_message_send(data)` - Announce in #backend-cell: "Docs complete for TASK-XXX, awaiting PM review"
**NOTE:** You do NOT complete the task. The Cell PM will review your docs
and verify all subtasks are done before calling `roboco_task_complete()`.
### 8. DOCUMENT
`roboco_journal_reflect(data)` - Document your documentation work
@@ -131,7 +135,7 @@ capabilities:
tools:
- roboco_task_scan, roboco_task_get, roboco_task_claim
- 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_journal_entry, roboco_journal_reflect
- roboco_journal_decision, roboco_journal_learning
@@ -158,6 +162,6 @@ permissions:
task_permissions:
- claim_doc_tasks
- complete_tasks
- mark_docs_complete # NOT complete_tasks (that's PM only)
- escalate_tasks
```
+16 -2
View File
@@ -40,7 +40,7 @@ You interact with RoboCo systems through MCP tools:
- `roboco_task_claim(task_id)` - Claim a task for triage
- `roboco_task_start(task_id)` - Start working on a task (moves to in_progress)
- `roboco_task_plan(task_id, plan)` - Add your triage plan to the task
- `roboco_task_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_assign(task_id, agent_slug)` - Assign task to an agent
- `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
- 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:
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
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
### Channels You Access
+34 -7
View File
@@ -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_claim(task_id)` - Claim a task for review
- `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_fail(task_id, qa_notes, issues)` - Reject task with issues (QA only)
- `roboco_task_escalate(task_id, reason)` - Escalate issues to PM
@@ -82,9 +82,19 @@ You interact with RoboCo systems through MCP tools:
### 3. UNDERSTAND
**Tool:** `roboco_task_get(task_id)` provides full context
- Read task requirements and acceptance criteria
- Read dev's notes and handoff summary
- Review commits and code changes
**What you can see:**
- 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
### 4. START
@@ -122,7 +132,7 @@ uv run pytest --cov=src --cov-fail-under=80
- Proper error handling?
- 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)`
### 6. VERDICT
@@ -140,14 +150,20 @@ roboco_task_qa_pass(task_id, {
```json
{
"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"
}
```
#### FAIL
**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
roboco_task_qa_fail(task_id, {
"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)`
```json
{
+31 -8
View File
@@ -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_start(task_id)` - Begin work (moves to in_progress)
- `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_unblock(task_id)` - Resume from blocked state
- `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
Work through your plan:
- **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
- Journal learnings: `roboco_journal_learning(data)`
- Journal struggles: `roboco_journal_struggle(data)`
@@ -152,15 +152,38 @@ roboco_task_pause(task_id, {
- All checks MUST pass before proceeding
### 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_journal_reflect(data)`
Document what you did, learned, struggled with.
This is what QA uses to verify your work. Include:
- What you built and where (components, files)
- Key implementation decisions
- Tests added, accessibility notes
- Any gotchas or important context
### 9. CLOSE
- After QA approval + Documentation complete
- Task transitions to "completed" automatically
- Return to SCAN: `roboco_task_scan()` or `roboco_agent_idle()`
```python
roboco_task_submit_qa(task_id, {
"dev_notes": "Built modal component with form validation. Used React Hook Form for state. Added 8 tests covering all states.",
"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
+11 -7
View File
@@ -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_claim(task_id)` - Claim for documentation
- `roboco_task_start(task_id)` - Begin documentation work
- `roboco_task_progress(task_id, message)` - Update progress
- `roboco_task_complete(task_id)` - Mark documentation complete
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
- `roboco_task_docs_complete(task_id, doc_notes?)` - Mark docs done (goes to PM review)
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
**Journal:**
@@ -106,9 +106,13 @@ If none: `roboco_agent_idle()`
- {Description}
```
### 7. COMPLETE
`roboco_task_complete(task_id)` - Mark task as completed
`roboco_message_send(data)` - Announce in #frontend-cell
### 7. SUBMIT TO PM
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
This sends the task to the Cell PM for final review and completion.
`roboco_message_send(data)` - Announce in #frontend-cell: "Docs complete for TASK-XXX, awaiting PM review"
**NOTE:** You do NOT complete the task. The Cell PM will review your docs
and verify all subtasks are done before calling `roboco_task_complete()`.
### 8. DOCUMENT
`roboco_journal_reflect(data)` - Document your documentation work
@@ -129,7 +133,7 @@ capabilities:
tools:
- roboco_task_scan, roboco_task_get, roboco_task_claim
- 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_journal_entry, roboco_journal_reflect
- roboco_journal_decision, roboco_journal_learning
@@ -156,6 +160,6 @@ permissions:
task_permissions:
- claim_doc_tasks
- complete_tasks
- mark_docs_complete # NOT complete_tasks (that's PM only)
- escalate_tasks
```
+16 -2
View File
@@ -41,7 +41,7 @@ You interact with RoboCo systems through MCP tools:
- `roboco_task_claim(task_id)` - Claim a task for triage
- `roboco_task_start(task_id)` - Start working on a task (moves to in_progress)
- `roboco_task_plan(task_id, plan)` - Add your triage plan to the task
- `roboco_task_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_assign(task_id, agent_slug)` - Assign task to an agent
- `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
- 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:
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
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
### With Backend (BE-PM)
+12 -2
View File
@@ -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_claim(task_id)` - Claim for review
- `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_fail(task_id, qa_notes, issues)` - Reject with issues
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
@@ -74,6 +74,16 @@ If none: `roboco_agent_idle()`
### 3. UNDERSTAND
`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
`roboco_task_start(task_id)` - Required before adding progress notes
@@ -97,7 +107,7 @@ If none: `roboco_agent_idle()`
- Chrome, Firefox, Safari
- 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
**PASS:** `roboco_task_qa_pass(task_id, qa_notes)`
+27 -10
View File
@@ -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_start(task_id)` - Begin work (moves to in_progress)
- `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_unblock(task_id)` - Resume from blocked state
- `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)
- Design for all breakpoints (mobile, tablet, desktop)
- 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 learnings: `roboco_journal_learning(data)`
@@ -151,21 +151,38 @@ Checklist:
- Edge cases handled
### 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)`
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
roboco_task_submit_qa(task_id, {
"dev_notes": "Used segmented control for theme toggle. All states in Figma.",
"handoff_summary": "Figma link: [link]. Mobile-first, responsive. All states complete."
"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: default, hover, active, disabled, loading."
})
```
**Tool:** `roboco_journal_reflect(data)`
Document what you designed, decisions made, what you learned.
**Tool:** `roboco_journal_reflect(data)` (Personal - QA cannot see this)
### 9. CLOSE
- After QA approval + Documentation complete
- Task transitions to "completed" automatically
- Return to SCAN: `roboco_task_scan()` or `roboco_agent_idle()`
Document what you designed, decisions made, what you learned 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
+11 -7
View File
@@ -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_claim(task_id)` - Claim for documentation
- `roboco_task_start(task_id)` - Begin documentation work
- `roboco_task_progress(task_id, message)` - Update progress
- `roboco_task_complete(task_id)` - Mark documentation complete
- `roboco_task_progress(task_id, message, percentage)` - Update progress (percentage 0-100 required)
- `roboco_task_docs_complete(task_id, doc_notes?)` - Mark docs done (goes to PM review)
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
**Journal:**
@@ -106,9 +106,13 @@ If none: `roboco_agent_idle()`
- {Description}
```
### 7. COMPLETE
`roboco_task_complete(task_id)` - Mark task as completed
`roboco_message_send(data)` - Announce in #uxui-cell
### 7. SUBMIT TO PM
`roboco_task_docs_complete(task_id, doc_notes?)` - Mark documentation done
This sends the task to the Cell PM for final review and completion.
`roboco_message_send(data)` - Announce in #uxui-cell: "Docs complete for TASK-XXX, awaiting PM review"
**NOTE:** You do NOT complete the task. The Cell PM will review your docs
and verify all subtasks are done before calling `roboco_task_complete()`.
### 8. DOCUMENT
`roboco_journal_reflect(data)` - Document your documentation work
@@ -129,7 +133,7 @@ capabilities:
tools:
- roboco_task_scan, roboco_task_get, roboco_task_claim
- 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_journal_entry, roboco_journal_reflect
- roboco_journal_decision, roboco_journal_learning
@@ -156,6 +160,6 @@ permissions:
task_permissions:
- claim_doc_tasks
- complete_tasks
- mark_docs_complete # NOT complete_tasks (that's PM only)
- escalate_tasks
```
+16 -2
View File
@@ -41,7 +41,7 @@ You interact with RoboCo systems through MCP tools:
- `roboco_task_claim(task_id)` - Claim a task for triage
- `roboco_task_start(task_id)` - Start working on a task (moves to in_progress)
- `roboco_task_plan(task_id, plan)` - Add your triage plan to the task
- `roboco_task_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_assign(task_id, agent_slug)` - Assign task to an agent
- `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
- 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:
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
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
### With Frontend (FE-PM)
+11 -1
View File
@@ -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_claim(task_id)` - Claim for review
- `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_fail(task_id, qa_notes, issues)` - Reject with issues
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
@@ -74,6 +74,16 @@ If none: `roboco_agent_idle()`
### 3. UNDERSTAND
`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
`roboco_task_start(task_id)` - Required before adding notes
+88 -80
View File
@@ -31,8 +31,8 @@ class DeveloperAgent(Agent):
4. PLAN - Break into subtasks, create plan
5. EXECUTE - Work through subtasks, commit frequently
6. VERIFY - Self-test, run quality checks
7. NOTES - Document journey, create handoff
8. CLOSE - After QA approval, mark complete
7. NOTES - Document journey, create handoff, submit for QA
8. DONE - Return to SCAN (QA → Documenter → PM complete the task)
"""
def __init__(self, config: AgentConfig) -> None:
@@ -96,7 +96,8 @@ class DeveloperAgent(Agent):
"""
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
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.VERIFY: self._handle_verify_phase,
DevTaskPhase.NOTES: self._handle_notes_phase,
DevTaskPhase.CLOSE: self._handle_close_phase,
DevTaskPhase.BLOCKED: self._handle_blocked_phase,
}
handler = phase_handlers.get(ctx.phase)
@@ -169,14 +169,11 @@ class DeveloperAgent(Agent):
return False
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)
ctx.phase = DevTaskPhase.CLOSE
return False
async def _handle_close_phase(self, ctx: TaskContext) -> bool:
"""Handle CLOSE phase transition."""
return await self._phase_close(ctx)
# Developer is done - task is now awaiting_qa
# QA → Documenter → PM will complete the task
return True
async def _handle_blocked_phase(self, ctx: TaskContext) -> bool:
"""Handle BLOCKED phase transition."""
@@ -385,11 +382,18 @@ Respond with the implementation.
commit_hash = f"commit_{ctx.current_subtask}"
ctx.commits.append(commit_hash)
# Progress update
progress = f"{ctx.current_subtask + 1}/{len(ctx.subtasks)}"
# Progress update - save to task AND send message
completed = ctx.current_subtask + 1
total = len(ctx.subtasks)
percentage = int((completed / total) * 100) if total > 0 else 0
progress_msg = f"Completed subtask {completed}/{total}: {subtask['title']}"
# Save progress to task (QA will see this!)
await self._add_progress(ctx.task_id, progress_msg, percentage)
await self.send_message(
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",
)
@@ -442,63 +446,47 @@ Respond with the implementation.
"""
NOTES phase: Document journey and create handoff.
- Complete journey notes
- Complete journey notes (stored in task dev_notes for QA)
- Link commits
- Create documenter handoff
- Create documenter handoff summary
"""
self.log.info("NOTES phase", task_id=str(ctx.task_id))
# Generate handoff using LLM
prompt = f"""
Create a documentation handoff for this completed task:
# Generate dev_notes for QA verification (what was built, where, key decisions)
dev_notes_prompt = f"""
Summarize the work done for QA verification:
Task: {ctx.title}
Commits: {", ".join(ctx.commits)}
Journal:
Work log:
{chr(10).join(ctx.journal_entries)}
Create a handoff summary including:
1. What was built
2. Key changes
3. Documentation needed
4. Code samples to include
Create a brief summary for QA including:
1. What was built and where (files/modules)
2. Key implementation decisions
3. Tests added
4. Any gotchas or important context
"""
_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(
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:
"""
Handle blocked state.
@@ -603,32 +591,52 @@ Create a handoff summary including:
except Exception as e:
self.log.error("Failed to update task status", error=str(e))
async def _check_qa_approved(self, task_id: UUID) -> bool:
"""Check if QA has approved the task."""
try:
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 _add_progress(self, task_id: UUID, message: str, percentage: int) -> None:
"""
Add progress update to task.
async def _check_docs_complete(self, task_id: UUID) -> bool:
"""Check if documentation is complete."""
This is saved to task.progress_updates and visible to QA.
Percentage is required (0-100) to show real progress.
"""
try:
result = await self._api_call("GET", f"/tasks/{task_id}/handoffs")
handoffs = result.get("items", [])
# Check if documenter handoff is complete
for handoff in handoffs:
is_doc = handoff.get("type") == "documentation"
is_done = handoff.get("status") == "completed"
if is_doc and is_done:
return True
return False
await self._api_call(
"POST",
f"/tasks/{task_id}/progress",
json={
"message": message,
"percentage": percentage,
},
)
self.log.info("Progress saved", task_id=str(task_id), percentage=percentage)
except Exception as e:
self.log.warning("Failed to check docs status", error=str(e))
return False
self.log.warning("Failed to save progress", error=str(e))
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(
+20 -6
View File
@@ -404,12 +404,12 @@ good,complete,clear,helpful,None
"Failed to publish", path=doc_spec.path, error=str(e)
)
# Update task status
await self._update_task_status(ctx.task_id, TaskStatus.COMPLETED)
# Mark docs complete - task goes to PM for final review
await self._update_task_status(ctx.task_id, TaskStatus.AWAITING_PM_REVIEW)
await self.send_message(
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)}",
message_type="action",
)
@@ -446,11 +446,25 @@ good,complete,clear,helpful,None
return f"Task {str(task_id)[:8]}"
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:
result = await self._api_call("GET", f"/tasks/{task_id}")
notes: str = result.get("dev_notes", "No developer notes available")
return notes
notes: str = result.get("dev_notes") or ""
# 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:
self.log.warning("Failed to read dev notes", error=str(e))
return "Dev notes unavailable"
+17 -3
View File
@@ -453,11 +453,25 @@ PASS,All criteria verified successfully,No issues found
return "Requirements unavailable"
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:
result = await self._api_call("GET", f"/tasks/{task_id}")
notes: str = result.get("dev_notes", "No developer notes available")
return notes
notes: str = result.get("dev_notes") or ""
# 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:
self.log.warning("Failed to read dev notes", error=str(e))
return "Dev notes unavailable"
+15
View File
@@ -188,6 +188,21 @@ def can_assign_tasks(agent_id: str) -> bool:
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:
"""Get the escalation target for an agent."""
return ESCALATION_CHAIN.get(agent_id)
+111 -10
View File
@@ -22,6 +22,7 @@ from roboco.api.schemas.tasks import (
ListTasksQuery,
ProgressRequest,
QANotes,
SoftBlockRequest,
TaskCountResponse,
TaskResponse,
TaskUpdate,
@@ -528,6 +529,51 @@ async def block_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)
async def unblock_task(
task_id: UUID,
@@ -787,14 +833,18 @@ async def fail_qa(
return task_to_response(task)
@router.post("/{task_id}/complete", response_model=TaskResponse)
async def complete_task(
@router.post("/{task_id}/docs-complete", response_model=TaskResponse)
async def docs_complete(
task_id: UUID,
db: DbSession,
agent: CurrentAgentContext,
permissions: PermissionServiceDep,
data: QANotes | None = None,
) -> 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)
task = await service.get(task_id)
if not task:
@@ -802,14 +852,65 @@ async def complete_task(
status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
)
# Check close permission - assigned agent or those with CLOSE permission
is_assigned = task.assigned_to == agent.agent_id
can_close = permissions.can_perform_task_action(agent, TaskAction.CLOSE, task.team)
if not (is_assigned or can_close):
# Only documenter role can mark docs complete
if agent.role.value != "documenter":
raise HTTPException(
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)
+10
View File
@@ -289,6 +289,16 @@ class QANotes(BaseModel):
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):
"""Task count by category."""
+16 -3
View File
@@ -36,8 +36,10 @@ VALID_TRANSITIONS: dict[str, list[str]] = {
"needs_revision": ["in_progress", "cancelled"],
# Awaiting QA - can pass (to docs), fail (needs revision), block, or cancel
"awaiting_qa": ["awaiting_documentation", "needs_revision", "blocked", "cancelled"],
# Awaiting documentation - can complete or cancel
"awaiting_documentation": ["completed", "cancelled"],
# Awaiting documentation - documenter marks docs done, goes to PM review
"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
"completed": [],
"cancelled": [],
@@ -57,6 +59,10 @@ ROLE_RESTRICTED_TRANSITIONS: dict[tuple[str, str], list[str]] = {
# Only QA can pass or fail QA
("awaiting_qa", "awaiting_documentation"): ["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)
("pending", "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,
("awaiting_qa", "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:
"""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:
+68 -60
View File
@@ -4,11 +4,18 @@ Notify MCP Server
Exposes notification tools to Claude Code agents with built-in
enforcement of notification permissions.
Tools:
Tools available to ALL agents:
- roboco_notify_list: List your notifications
- roboco_notify_get: Get a specific 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
@@ -18,6 +25,7 @@ from mcp.server.fastmcp import FastMCP
from roboco.agents_config import (
NOTIFICATION_PERMISSIONS,
can_send_notifications,
get_agent_cell,
get_agent_role,
)
@@ -301,71 +309,71 @@ def create_notify_mcp_server(agent_id: str) -> FastMCP:
"""Acknowledge a notification."""
return await _handle_ack(client, notification_id)
@mcp.tool()
async def roboco_notify_send(data: SendNotificationInput) -> dict[str, Any]:
"""
Send a notification to one or more agents.
# Only register send/escalate/approval tools for agents who can send notifications
# This prevents developers, QA, and documenters from even seeing these tools
if can_send_notifications(agent_id):
Only PMs, Board members, and Auditor can send notifications.
Cell PMs can only notify their own cell.
"""
return await _handle_send(client, agent_id, data)
@mcp.tool()
async def roboco_notify_send(data: SendNotificationInput) -> dict[str, Any]:
"""
Send a notification to one or more agents.
@mcp.tool()
async def roboco_escalate(
escalate_to: str,
subject: str,
description: str,
task_id: str | None = None,
) -> dict[str, Any]:
"""
Escalate an issue to a higher level (PM only).
Cell PMs can only notify their own cell.
Main PM, Board, and Auditor can notify anyone.
"""
return await _handle_send(client, agent_id, data)
Sends a high-priority notification requiring acknowledgment.
"""
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(
recipients=[escalate_to],
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)
# Only PMs can escalate
if role in ["cell_pm", "main_pm"]:
@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 (PM/Board only).
"""
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"
)
@mcp.tool()
async def roboco_escalate(
escalate_to: str,
subject: str,
description: str,
task_id: str | None = None,
) -> dict[str, Any]:
"""
Escalate an issue to a higher level.
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)
Sends a high-priority notification requiring acknowledgment.
"""
input_data = SendNotificationInput(
recipients=[escalate_to],
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)
# 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
+326 -104
View File
@@ -17,9 +17,11 @@ Tools:
- roboco_task_submit_qa: Submit for QA review
- roboco_task_qa_pass: Pass 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_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)
"""
@@ -30,6 +32,7 @@ from mcp.server.fastmcp import FastMCP
from roboco.agents_config import (
can_assign_tasks,
can_cancel_tasks,
can_create_tasks,
get_agent_role,
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
_toon = ToonAdapter()
# Progress percentage bounds
_MIN_PERCENTAGE = 0
_MAX_PERCENTAGE = 100
# 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(
client: ApiClient, team: str | None, agent_id: str
) -> dict[str, Any]:
@@ -186,42 +244,15 @@ async def _handle_task_scan(
]
# Get available tasks based on agent role
# QA agents need awaiting_qa tasks, Documenters need awaiting_documentation
agent_role = get_agent_role(agent_id)
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()
available_tasks = await _get_available_tasks_for_role(client, agent_role, team)
# Filter out tasks already in assigned_tasks from available_tasks
# (prevents PM-assigned pending tasks from appearing in both lists)
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]
# Determine guidance
# Determine guidance based on role and available tasks
if paused_tasks:
guidance = (
f"You have {len(paused_tasks)} paused task(s). "
@@ -233,10 +264,7 @@ async def _handle_task_scan(
"Continue working on your assigned tasks."
)
elif available_tasks:
guidance = (
f"Found {len(available_tasks)} available task(s). "
"Review and claim one that matches your skills."
)
guidance = _get_available_tasks_guidance(available_tasks, agent_role)
else:
guidance = (
"No tasks available. Call roboco_agent_idle() "
@@ -492,30 +520,40 @@ async def _validate_task_start(
return error
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(
"INVALID_STATE",
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},
)
# Only require plan for newly claimed tasks, not for resuming revision
if task_status == "claimed" and not task.get("plan"):
return _format_error_response(
"NO_PLAN",
"Cannot start without a plan. Call roboco_task_plan first.",
)
plan = task.get("plan", {})
unanswered = [q for q in plan.get("open_questions", []) if not q.get("answered")]
if unanswered:
return _format_error_response(
"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]},
)
# Only check open questions for newly claimed tasks
if task_status == "claimed":
plan = task.get("plan", {})
unanswered = [
q for q in plan.get("open_questions", []) if not q.get("answered")
]
if unanswered:
return _format_error_response(
"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
@@ -559,10 +597,17 @@ async def _handle_task_progress(
client: ApiClient,
task_id: str,
message: str,
percentage: int | None,
percentage: int,
agent_id: str,
) -> dict[str, Any]:
"""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}")
if task_resp.is_status(status.HTTP_404_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):
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(
"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
@@ -608,7 +660,7 @@ async def _handle_task_block(
data: TaskBlockInput,
agent_id: str,
) -> dict[str, Any]:
"""Handle task blocking."""
"""Handle task blocking via the soft-block endpoint."""
task_resp = await client.get(f"/tasks/{data.task_id}")
if task_resp.is_status(status.HTTP_404_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",
)
# Build blocker note for dev_notes
blocker_note = (
f"[BLOCKED - {data.blocker_type.upper()}]\n"
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}",
# Use the soft-block endpoint which handles status change and notes
block_resp = await client.post(
f"/tasks/{data.task_id}/soft-block",
json={
"status": "blocked",
"dev_notes": updated_notes,
"reason": data.reason,
"blocker_type": data.blocker_type,
"what_needed": data.what_needed,
},
)
@@ -813,46 +854,50 @@ async def _handle_task_submit_qa(
agent_id: str,
) -> dict[str, Any]:
"""Handle task QA submission."""
# Validate inputs
if not dev_notes or not handoff_summary:
return _format_error_response(
"MISSING_NOTES",
"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}")
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 error := await _validate_task_ownership(task, agent_id, client):
return error
# Validate state
if task.get("status") != "verifying":
return _format_error_response(
"INVALID_STATE",
"Can only submit verified tasks for QA",
"INVALID_STATE", "Can only submit verified tasks for QA"
)
# Update with notes - combine dev_notes and handoff summary
# (handoff summary goes into dev_notes for documenter to read)
# Save dev notes and handoff summary, then submit for QA
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")
if not qa_resp.ok:
return _format_error_response("SUBMIT_FAILED", "Failed to submit for QA")
qa_task = qa_resp.json()
return _format_task_response(
qa_task,
"WAIT_FOR_QA",
"Task submitted for QA review.\n"
"You will be notified of the result.\n"
"In the meantime, call roboco_task_scan for other work.",
return (
_format_task_response(
qa_resp.json(),
"WAIT_FOR_QA",
"Task submitted for QA review.\n"
"You will be notified of the result.\n"
"In the meantime, call roboco_task_scan for other work.",
)
if qa_resp.ok
else _format_error_response("SUBMIT_FAILED", "Failed to submit for QA")
)
@@ -863,11 +908,13 @@ async def _handle_task_qa_pass(
agent_id: str,
) -> dict[str, Any]:
"""Handle task QA pass."""
# Check if agent has QA role (simple check - real impl would verify)
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(
"NOT_QA",
"Only QA agents can pass tasks through QA review.",
{"your_role": agent_role},
)
task_resp = await client.get(f"/tasks/{task_id}")
@@ -925,10 +972,13 @@ async def _handle_task_qa_fail(
agent_id: str,
) -> dict[str, Any]:
"""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(
"NOT_QA",
"Only QA agents can fail tasks in QA review.",
{"your_role": agent_role},
)
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]:
"""Handle task completion."""
async def _handle_docs_complete(
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}")
if task_resp.is_status(status.HTTP_404_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":
return _format_error_response(
"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")
@@ -994,10 +1107,7 @@ async def _handle_task_complete(client: ApiClient, task_id: str) -> dict[str, An
return _format_error_response(
"COMPLETE_FAILED",
"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()
@@ -1005,7 +1115,60 @@ async def _handle_task_complete(client: ApiClient, task_id: str) -> dict[str, An
return _format_task_response(
completed_task,
"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(
task_id: str,
message: str,
percentage: int | None = None,
percentage: int,
) -> dict[str, Any]:
"""
Update task progress.
ENFORCEMENT:
- Percentage is REQUIRED (0-100) to show real progress
- Message must describe what was accomplished
Args:
task_id: The task UUID
message: Progress update message
percentage: Optional completion percentage (0-100)
message: Progress update message describing work done
percentage: Completion percentage (0-100), required
Returns:
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)
@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:
- Only documenters can use this tool
- 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:
task_id: The task UUID
@@ -1699,7 +1894,7 @@ def create_task_mcp_server(agent_id: str) -> FastMCP:
Returns:
Completed task
"""
return await _handle_task_complete(client, task_id)
return await _handle_task_complete(client, task_id, agent_id)
@mcp.tool()
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)
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()
async def roboco_task_escalate(
task_id: str,
+2 -2
View File
@@ -97,11 +97,11 @@ async def resolve_agent_uuid(
if len(agent_id) == _UUID_LENGTH and agent_id.count("-") == _UUID_HYPHEN_COUNT:
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:
try:
resp = await client.get(
f"{settings.internal_api_url}/agents/by-slug/{agent_id}",
f"{settings.internal_api_url}/agents/{agent_id}",
headers=headers,
)
if resp.status_code == _HTTP_OK:
+1
View File
@@ -28,6 +28,7 @@ class TaskStatus(str, Enum):
NEEDS_REVISION = "needs_revision"
AWAITING_QA = "awaiting_qa"
AWAITING_DOCUMENTATION = "awaiting_documentation"
AWAITING_PM_REVIEW = "awaiting_pm_review" # After docs, before PM completes
COMPLETED = "completed"
CANCELLED = "cancelled"
+2 -2
View File
@@ -70,8 +70,8 @@ class WaitingRecord:
# Model mapping for cost optimization
MODEL_MAP: dict[str, str] = {
"sonnet": "claude-sonnet-4-20250514",
"opus": "claude-opus-4-20250514",
"haiku": "claude-haiku-4-20250514",
"opus": "claude-opus-4-5-20251101",
"haiku": "claude-haiku-4-5-20251001",
}
+67 -22
View File
@@ -25,6 +25,7 @@ import httpx
import structlog
from fastapi import status as http_status
from roboco.agents_config import get_agent_role
from roboco.config import settings
from roboco.models.runtime import (
MODEL_MAP,
@@ -200,23 +201,18 @@ class AgentOrchestrator:
# Journal - always needed for reflection
"mcp__roboco-journal__*",
# File operations for documenters and developers
"Write(path:/app/docs/**)",
"Write(path:/app/CHANGELOG.md)",
"Write(path:/app/README.md)",
"Edit(path:/app/docs/**)",
"Edit(path:/app/CHANGELOG.md)",
"Edit(path:/app/README.md)",
# Note: // prefix = absolute path (container paths like /app/docs)
"Write(//app/docs/**)",
"Write(//app/CHANGELOG.md)",
"Write(//app/README.md)",
"Edit(//app/docs/**)",
"Edit(//app/CHANGELOG.md)",
"Edit(//app/README.md)",
]
# Path to agent Claude settings (shared across all agents)
# When running in container: Claude auth is mounted to /root/.claude
# When running on host: use CLAUDE_AUTH_HOST_PATH directly
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)
# Always use CLAUDE_AUTH_HOST_PATH - agents mount from this location
claude_dir = Path(CLAUDE_AUTH_HOST_PATH)
settings_path = claude_dir / "settings.json"
@@ -298,10 +294,10 @@ class AgentOrchestrator:
# Generate MCP config
mcp_config_path = await self._generate_mcp_config(agent_id)
# Determine model
# Determine model using canonical role name from agents_config
if not model:
role = self._get_agent_role(agent_id)
model = ROLE_MODEL_MAP.get(role, "sonnet")
canonical_role = get_agent_role(agent_id)
model = ROLE_MODEL_MAP.get(canonical_role, "sonnet")
# Create config
config = AgentConfig(
@@ -540,7 +536,7 @@ class AgentOrchestrator:
def _get_blueprint_path(self, agent_id: str) -> Path:
"""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)
if team == "backend":
@@ -557,7 +553,7 @@ class AgentOrchestrator:
def _get_blueprint_rel_path(self, agent_id: str) -> str:
"""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)
if team == "backend":
@@ -572,8 +568,8 @@ class AgentOrchestrator:
blueprint_file = f"{role.replace('_', '-')}.md"
return f"{cell_dir}/{blueprint_file}"
def _get_agent_role(self, agent_id: str) -> str:
"""Get role from agent_id."""
def _get_blueprint_role(self, agent_id: str) -> str:
"""Get blueprint-specific role name from agent_id (used for file paths)."""
role_map = {
"be-dev-1": "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_qa_work(client)
await self._dispatch_doc_work(client)
await self._dispatch_pm_review_work(client)
await self._dispatch_marketing_work(client)
# Event-based dispatchers (check blockers, notifications)
@@ -1599,6 +1596,32 @@ Begin with step 1: roboco_task_get("{task_id}")
)
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:
"""
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
2. Create or update documentation based on what was implemented
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
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:
+11 -8
View File
@@ -34,6 +34,7 @@ from roboco.models.journal import (
create_struggle_entry,
create_task_reflection,
)
from roboco.models.optimal import IndexJournalEntryParams
from roboco.utils.converters import require_uuid, to_python_uuid
logger = structlog.get_logger()
@@ -242,14 +243,16 @@ class JournalService:
try:
optimal = await self._get_optimal_service()
await optimal.index_journal_entry(
entry_id=entry_row.id,
agent_id=journal_row.agent_id
if journal_row
else entry_create.journal_id,
content=entry_create.content,
entry_type=type_key,
task_id=entry_create.task_id,
tags=entry_create.tags,
IndexJournalEntryParams(
entry_id=entry_row.id,
agent_id=journal_row.agent_id
if journal_row
else entry_create.journal_id,
content=entry_create.content,
entry_type=type_key,
task_id=entry_create.task_id,
tags=entry_create.tags,
)
)
except Exception as e:
logger.warning("Failed to index journal entry in RAG", error=str(e))
+202 -43
View File
@@ -15,10 +15,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from roboco.db.tables import AgentTable, TaskTable
from roboco.enforcement import (
TaskLifecycleError,
TaskOwnershipError,
validate_task_ownership,
validate_task_transition,
)
from roboco.models.base import TaskStatus, Team
from roboco.models.task import TaskCreateRequest
@@ -180,19 +178,24 @@ class TaskService:
)
agent = agent_result.scalar_one_or_none()
# Base valid statuses for claiming
valid_statuses = {TaskStatus.PENDING}
if allow_reassign:
valid_statuses.add(TaskStatus.CLAIMED)
# Role-based claiming: each role can only claim specific statuses
# QA → awaiting_qa only, Documenter → awaiting_documentation only
# Developers/PMs → pending (and claimed if allow_reassign)
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:
role = agent.role.value if hasattr(agent.role, "value") else str(agent.role)
if role == "qa":
# QA can ONLY claim awaiting_qa tasks
valid_statuses.add(TaskStatus.AWAITING_QA)
elif role == "documenter":
# Documenter can ONLY claim awaiting_documentation tasks
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}:
# No role information - reject claims for role-specific statuses
logger.warning(
@@ -204,6 +207,11 @@ class TaskService:
agent_role="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:
logger.warning(
@@ -225,15 +233,18 @@ class TaskService:
)
return None
# For QA/Documenter claiming, store previous owner for self-review checks
# before changing assigned_to
# For QA/Documenter claiming, ensure original_developer is set for
# 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:
role = agent.role.value if hasattr(agent.role, "value") else str(agent.role)
if role in ("qa", "documenter"):
# Store original developer in quick_context for self-review check
original_dev = str(task.assigned_to) if task.assigned_to else None
if original_dev:
task.quick_context = f"original_developer:{original_dev}"
# Only set if not already stored by submit_for_qa
existing_context = task.quick_context or ""
if "original_developer:" not in existing_context:
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
task.assigned_to = cast("Any", agent_id)
@@ -290,7 +301,16 @@ class TaskService:
)
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(
"Cannot start task - invalid status",
task_id=str(task_id),
@@ -298,7 +318,9 @@ class TaskService:
)
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
await self.session.flush()
@@ -330,6 +352,61 @@ class TaskService:
)
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:
"""Unblock a task and resume to in_progress."""
task = await self.get(task_id)
@@ -399,11 +476,22 @@ class TaskService:
if task.status != TaskStatus.VERIFYING:
return None
# Store original developer BEFORE QA claims - this is the authoritative record
# for self-review prevention. Storing here ensures we capture the developer
# even if the task is reassigned before QA claims it.
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.status = TaskStatus.AWAITING_QA
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
async def pass_qa(
@@ -427,7 +515,13 @@ class TaskService:
return task
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)
if not task:
return None
@@ -438,25 +532,105 @@ class TaskService:
task.qa_notes = notes
task.qa_verified = False
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()
logger.info("Task failed QA", task_id=str(task_id))
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(
self,
task_id: UUID,
skip_handoff_check: bool = False,
) -> TaskTable | None:
"""
Mark task as completed.
Mark task as completed (PM only).
Enforces handoff requirement: tasks in AWAITING_DOCUMENTATION
must have an accepted handoff before completion.
Only PMs can complete tasks, and only from AWAITING_PM_REVIEW status.
This ensures the full workflow: Dev QA Documenter PM.
Args:
task_id: The task to complete
skip_handoff_check: Skip handoff requirement (for small tasks)
Returns:
The completed task or None if completion not allowed
@@ -465,28 +639,13 @@ class TaskService:
if not task:
return None
# Validate transition using enforcement layer
try:
validate_task_transition(task.status.value, TaskStatus.COMPLETED.value)
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
):
# Only allow completion from AWAITING_PM_REVIEW
# This enforces the workflow: documenter calls docs_complete, PM calls complete
if task.status != TaskStatus.AWAITING_PM_REVIEW:
logger.warning(
"Cannot complete task - QA verification required",
"Cannot complete task - must be in awaiting_pm_review status",
task_id=str(task_id),
status=task.status.value,
current_status=task.status.value,
)
return None