Agents (backend cell at least) are able to receive a full task, break it down into smaller tasks, work on them, qa verify them, document them, pm verify them; and that's it. Completed task.

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