mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Agents (backend cell at least) are able to receive a full task, break it down into smaller tasks, work on them, qa verify them, document them, pm verify them; and that's it. Completed task.
This commit is contained in:
+260
@@ -0,0 +1,260 @@
|
||||
# RoboCo Workflows & Permissions
|
||||
|
||||
Visual documentation of task lifecycles, permissions, and workflows.
|
||||
|
||||
## 1. Task Lifecycle State Machine
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending: Task Created
|
||||
|
||||
pending --> claimed: Developer claims
|
||||
pending --> cancelled: PM cancels
|
||||
|
||||
claimed --> in_progress: Developer starts
|
||||
claimed --> pending: Developer unclaims
|
||||
claimed --> cancelled: PM cancels
|
||||
|
||||
in_progress --> blocked: Developer blocked
|
||||
in_progress --> paused: Developer pauses
|
||||
in_progress --> verifying: Developer self-verifies
|
||||
in_progress --> cancelled: PM cancels
|
||||
|
||||
blocked --> in_progress: Unblocked
|
||||
blocked --> cancelled: PM cancels
|
||||
|
||||
paused --> in_progress: Developer resumes
|
||||
paused --> cancelled: PM cancels
|
||||
|
||||
verifying --> awaiting_qa: Submit for QA
|
||||
verifying --> needs_revision: Self-found issues
|
||||
verifying --> awaiting_documentation: Skip QA (small tasks)
|
||||
verifying --> cancelled: PM cancels
|
||||
|
||||
awaiting_qa --> awaiting_documentation: QA PASS
|
||||
awaiting_qa --> needs_revision: QA FAIL
|
||||
awaiting_qa --> blocked: Blocked during QA
|
||||
awaiting_qa --> cancelled: PM cancels
|
||||
|
||||
needs_revision --> in_progress: Developer resumes
|
||||
needs_revision --> cancelled: PM cancels
|
||||
|
||||
awaiting_documentation --> awaiting_pm_review: Documenter marks docs done
|
||||
awaiting_documentation --> cancelled: PM cancels
|
||||
|
||||
awaiting_pm_review --> completed: PM completes
|
||||
awaiting_pm_review --> cancelled: PM cancels
|
||||
|
||||
completed --> [*]
|
||||
cancelled --> [*]
|
||||
|
||||
quarantined --> pending: Un-quarantined
|
||||
```
|
||||
|
||||
## 2. Agent Hierarchy & Roles
|
||||
|
||||
```
|
||||
+-------+
|
||||
| CEO |
|
||||
+-------+
|
||||
|
|
||||
+---------------+---------------+
|
||||
| | |
|
||||
+---------+ +-----------+ +---------+
|
||||
| Product | | Head | | Auditor |
|
||||
| Owner | | Marketing | | (silent)|
|
||||
+---------+ +-----------+ +---------+
|
||||
| | |
|
||||
+-------+-------+ |
|
||||
| |
|
||||
+---------+ |
|
||||
| Main PM |<-----------------+
|
||||
+---------+ (observes all)
|
||||
|
|
||||
+-----------+-----------+
|
||||
| | |
|
||||
+-------+ +-------+ +-------+
|
||||
| BE PM | | FE PM | | UX PM |
|
||||
+-------+ +-------+ +-------+
|
||||
| | |
|
||||
+-------+ +-------+ +-------+
|
||||
|Backend| |Frontend| | UX/UI |
|
||||
| Cell | | Cell | | Cell |
|
||||
+-------+ +-------+ +-------+
|
||||
|
||||
Each Cell:
|
||||
- 2 Developers (BE/FE) or 1 Developer (UX)
|
||||
- 1 QA Engineer
|
||||
- 1 Documenter
|
||||
- 1 Cell PM
|
||||
```
|
||||
|
||||
## 3. Notification Permissions
|
||||
|
||||
```
|
||||
WHO CAN SEND NOTIFICATIONS:
|
||||
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
| Sender Role | Can Send? | Scope |
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
| CEO | YES | Anyone |
|
||||
| Auditor | YES | Anyone |
|
||||
| Main PM | YES | Anyone |
|
||||
| Product Owner | YES | main-pm, head-marketing, auditor, ceo |
|
||||
| Head Marketing | YES | main-pm, product-owner, auditor, ceo |
|
||||
| Cell PM | YES | Own cell only |
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
| Developer | NO | - |
|
||||
| QA | NO | - |
|
||||
| Documenter | NO | - |
|
||||
+------------------+-------------+----------------------------------------------+
|
||||
|
||||
TOOLS VISIBILITY:
|
||||
|
||||
+----------------------+------------+----------+---------+---------+---------+
|
||||
| Tool | Dev/QA/Doc | Cell PM | Main PM | Board | Aud/CEO |
|
||||
+----------------------+------------+----------+---------+---------+---------+
|
||||
| roboco_notify_list | YES | YES | YES | YES | YES |
|
||||
| roboco_notify_get | YES | YES | YES | YES | YES |
|
||||
| roboco_notify_ack | YES | YES | YES | YES | YES |
|
||||
| roboco_notify_send | HIDDEN | YES | YES | YES | YES |
|
||||
| roboco_escalate | HIDDEN | YES | YES | HIDDEN | HIDDEN |
|
||||
| roboco_request_appr | HIDDEN | YES | YES | YES | HIDDEN |
|
||||
+----------------------+------------+----------+---------+---------+---------+
|
||||
|
||||
Note: "Board" = Product Owner + Head Marketing. Auditor/CEO can send but not escalate or request approval.
|
||||
```
|
||||
|
||||
## 4. QA Fail → Revision Workflow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
participant Task as Task System
|
||||
participant QA as QA Engineer
|
||||
participant PM as Cell PM
|
||||
|
||||
Dev->>Task: Submit for QA (awaiting_qa)
|
||||
Note over Task: assigned_to = QA<br/>quick_context = original_developer:Dev
|
||||
|
||||
QA->>Task: Claim task
|
||||
QA->>Task: Review work
|
||||
|
||||
alt QA PASS
|
||||
QA->>Task: roboco_task_qa_pass()
|
||||
Task->>Task: status = awaiting_documentation
|
||||
Note over Task: Documenter claims and writes docs
|
||||
Note over Task: Documenter calls roboco_task_docs_complete()
|
||||
Task->>Task: status = awaiting_pm_review
|
||||
Note over Task: PM claims, reviews, calls roboco_task_complete()
|
||||
Task->>Task: status = completed
|
||||
else QA FAIL
|
||||
QA->>Task: roboco_task_qa_fail(issues)
|
||||
Task->>Task: status = needs_revision
|
||||
Task->>Task: assigned_to = original Dev (from quick_context)
|
||||
Note over Dev: Dev sees task in needs_revision
|
||||
Dev->>Task: roboco_task_start()
|
||||
Task->>Task: status = in_progress
|
||||
Dev->>Task: Fix issues, resubmit
|
||||
end
|
||||
```
|
||||
|
||||
## 5. Block/Unblock Workflow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Dev as Developer
|
||||
participant Task as Task System
|
||||
participant PM as Cell PM
|
||||
|
||||
Dev->>Task: Working on task (in_progress)
|
||||
|
||||
Note over Dev: Encounters blocker
|
||||
|
||||
Dev->>Task: roboco_task_block(reason, type, what_needed)
|
||||
Task->>Task: POST /tasks/{id}/soft-block
|
||||
Task->>Task: status = blocked
|
||||
Task->>Task: dev_notes += blocker info
|
||||
|
||||
Note over Dev: Can work on other tasks
|
||||
|
||||
alt Blocker resolved
|
||||
Dev->>Task: roboco_task_unblock()
|
||||
Task->>Task: POST /tasks/{id}/unblock
|
||||
Task->>Task: status = in_progress
|
||||
Dev->>Task: Continue working
|
||||
else Need PM help
|
||||
Dev->>PM: roboco_report_blocker() via message channel
|
||||
PM->>Task: Resolves blocker
|
||||
Dev->>Task: roboco_task_unblock()
|
||||
end
|
||||
```
|
||||
|
||||
## 6. Task Role Restrictions
|
||||
|
||||
```
|
||||
ROLE-BASED TRANSITIONS:
|
||||
|
||||
+-------------------------------+-------------------------------------------+
|
||||
| Transition | Allowed Roles |
|
||||
+-------------------------------+-------------------------------------------+
|
||||
| awaiting_qa → awaiting_doc | QA only |
|
||||
| awaiting_qa → needs_rev | QA only |
|
||||
| awaiting_doc → awaiting_pm | Documenter only |
|
||||
| awaiting_pm → completed | Cell PM, Main PM, Product Owner, Head Mkt |
|
||||
| * → cancelled | Cell PM, Main PM, Product Owner, Head Mkt |
|
||||
+-------------------------------+-------------------------------------------+
|
||||
|
||||
Note: CEO and Auditor are NOT in the cancel/complete roles list - they observe but don't directly act on tasks.
|
||||
|
||||
VALID START STATUSES (for roboco_task_start):
|
||||
|
||||
+------------------+------------------------------------------+
|
||||
| Status | Who Can Start |
|
||||
+------------------+------------------------------------------+
|
||||
| claimed | Assigned developer (requires plan) |
|
||||
| paused | Assigned developer (resume) |
|
||||
| needs_revision | Original developer (fix QA issues) |
|
||||
+------------------+------------------------------------------+
|
||||
```
|
||||
|
||||
## 7. Escalation Chain
|
||||
|
||||
```
|
||||
Developer/QA/Doc → Cell PM → Main PM → Product Owner → CEO
|
||||
|
||||
+------------+ +---------+ +---------+ +---------------+ +-----+
|
||||
| be-dev-1 |---->| | | | | | | |
|
||||
| be-dev-2 |---->| be-pm |---->| | | | | |
|
||||
| be-qa |---->| | | | | | | |
|
||||
| be-doc |---->| | | | | | | |
|
||||
+------------+ +---------+ | | | | | |
|
||||
| main-pm |---->| product-owner |---->| CEO |
|
||||
+------------+ +---------+ | | | | | |
|
||||
| fe-dev-1 |---->| | | | | | | |
|
||||
| fe-dev-2 |---->| fe-pm |---->| | | | | |
|
||||
| fe-qa |---->| | | | | | | |
|
||||
| fe-doc |---->| | | | | | | |
|
||||
+------------+ +---------+ +---------+ +---------------+ +-----+
|
||||
```
|
||||
|
||||
## 8. Communication vs Notification
|
||||
|
||||
```
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Mechanism | Who Can Use | Purpose |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Messages | Everyone | Constant stream, logged |
|
||||
| (roboco_message) | | discussions, updates |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Blocker Reports | Everyone | Signal blocked status |
|
||||
| (roboco_report_ | | PM auto-notified |
|
||||
| blocker) | | |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Notifications | PM, Board, Auditor, CEO | Formal signals requiring |
|
||||
| (roboco_notify) | | acknowledgment |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
| Escalations | PMs only | High-priority issues |
|
||||
| (roboco_escalate) | | up the chain |
|
||||
+-------------------+----------------------------------+----------------------------------+
|
||||
```
|
||||
@@ -40,7 +40,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- `roboco_task_claim(task_id)` - Claim a pending task
|
||||
- `roboco_task_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({
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user