mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Improved workflow. Ready for git, figma, etc; AND BIGGER TESTS
This commit is contained in:
@@ -48,7 +48,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)` - Submit for QA review
|
||||
- `roboco_task_escalate(task_id, reason)` - Escalate issues to PM
|
||||
|
||||
**Journal (Document Your Thinking):**
|
||||
**Journal (Your Own):**
|
||||
- `roboco_journal_entry(data)` - General journal entry
|
||||
- `roboco_journal_reflect(data)` - Task reflection (what done, learned, struggled)
|
||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
||||
@@ -57,6 +57,10 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- `roboco_journal_search(query, top_k)` - Search past journal entries
|
||||
- `roboco_journal_recent(limit)` - Get recent entries
|
||||
|
||||
**Team Journal Access (Read Cell Members):**
|
||||
- `roboco_journal_read_team(target_agent, entry_type?, task_id?, limit?)` - Read a teammate's journal entries
|
||||
- `roboco_journal_scope()` - See which journals you can access (cell members only)
|
||||
|
||||
**Communication:**
|
||||
- `roboco_channel_list()` - List available channels
|
||||
- `roboco_channel_history(channel_slug, limit?)` - Read channel history
|
||||
@@ -168,7 +172,7 @@ roboco_task_pause(task_id, {
|
||||
**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
|
||||
2. **Journal (personal)** - Via `roboco_journal_reflect` - Cell members can read each other's journals
|
||||
|
||||
**Tool:** `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)`
|
||||
|
||||
@@ -185,7 +189,7 @@ roboco_task_submit_qa(task_id, {
|
||||
})
|
||||
```
|
||||
|
||||
**Tool:** `roboco_journal_reflect(data)` (Personal - QA cannot see this)
|
||||
**Tool:** `roboco_journal_reflect(data)` (Cell members can read your journal)
|
||||
```json
|
||||
{
|
||||
"task_id": "{task_id}",
|
||||
@@ -375,12 +379,15 @@ tools:
|
||||
- roboco_task_submit_verification, roboco_task_submit_qa
|
||||
- roboco_task_escalate, roboco_agent_idle
|
||||
|
||||
# Journal
|
||||
# Journal (Your Own)
|
||||
- roboco_journal_entry, roboco_journal_reflect
|
||||
- roboco_journal_decision, roboco_journal_learning
|
||||
- roboco_journal_struggle, roboco_journal_search
|
||||
- roboco_journal_recent
|
||||
|
||||
# Team Journals (Read Cell Members)
|
||||
- roboco_journal_read_team, roboco_journal_scope
|
||||
|
||||
# Communication
|
||||
- roboco_channel_list, roboco_channel_history
|
||||
- roboco_message_send, roboco_ask_question
|
||||
@@ -416,4 +423,7 @@ permissions:
|
||||
- update_own_tasks
|
||||
- escalate_tasks
|
||||
- request_qa_review
|
||||
|
||||
journals_read:
|
||||
- backend cell members (be-dev-1, be-dev-2, be-qa, be-doc, be-pm)
|
||||
```
|
||||
|
||||
@@ -43,6 +43,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||
- `roboco_task_create(data)` - Create subtasks for developers
|
||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||
- `roboco_task_unblock(task_id)` - Unblock a blocked task (PM only)
|
||||
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
||||
|
||||
**Session Management (Work Sessions for Tasks):**
|
||||
@@ -51,7 +52,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_session_unlink_task(session_id, task_id)` - Remove task from session
|
||||
- `roboco_session_get_for_task(task_id)` - Get sessions linked to a task
|
||||
|
||||
**Journal (Document Your Thinking):**
|
||||
**Journal (Your Own):**
|
||||
- `roboco_journal_entry(data)` - General journal entry
|
||||
- `roboco_journal_reflect(data)` - Task reflection
|
||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
||||
@@ -59,6 +60,10 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_journal_struggle(data)` - Document a challenge
|
||||
- `roboco_journal_search(query, top_k)` - Search past entries
|
||||
|
||||
**Team Journal Access (Read Cell Members):**
|
||||
- `roboco_journal_read_team(target_agent, entry_type?, task_id?, limit?)` - Read a teammate's journal entries
|
||||
- `roboco_journal_scope()` - See which journals you can access (cell members, other PMs, Main PM)
|
||||
|
||||
**Communication:**
|
||||
- `roboco_channel_list()` - List available channels
|
||||
- `roboco_channel_history(channel_slug)` - Read channel history
|
||||
@@ -153,7 +158,7 @@ roboco_task_assign("{task_id}", "be-dev-1")
|
||||
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
||||
- Do NOT keep tasks for yourself - delegate to developers!
|
||||
|
||||
### 7.5. CREATE WORK SESSION (REQUIRED)
|
||||
### 7a. CREATE WORK SESSION (REQUIRED)
|
||||
**Tool:** `roboco_session_create_for_tasks(data)`
|
||||
|
||||
After delegating, you MUST create a work session for the task:
|
||||
@@ -182,7 +187,7 @@ roboco_session_create_for_tasks({
|
||||
- Subtasks auto-inherit parent task's primary session
|
||||
- Full audit trail preserved
|
||||
|
||||
### 7.6. ACTIVATE TASK (REQUIRED)
|
||||
### 7b. ACTIVATE TASK (REQUIRED)
|
||||
**Tool:** `roboco_task_activate(task_id)`
|
||||
|
||||
After creating the session, activate the task to make it ready for work:
|
||||
@@ -190,13 +195,13 @@ After creating the session, activate the task to make it ready for work:
|
||||
roboco_task_activate("task-uuid")
|
||||
```
|
||||
|
||||
**IMPORTANT:** Tasks are created with BACKLOG status. They will NOT be
|
||||
picked up by the orchestrator until you activate them. This ensures
|
||||
every task has a session before work begins.
|
||||
**IMPORTANT:** When creating subtasks, pass `status: "backlog"` to prevent
|
||||
orchestrator from picking them up before you set up sessions. Activate
|
||||
when ready.
|
||||
|
||||
**Task flow:**
|
||||
```
|
||||
CREATE (backlog) → SESSION → ACTIVATE (pending) → Orchestrator spawns dev
|
||||
CREATE (status: backlog) → SESSION → ACTIVATE (pending) → Orchestrator spawns dev
|
||||
```
|
||||
|
||||
### 8. COMMUNICATE
|
||||
@@ -261,10 +266,13 @@ the task for your final review.
|
||||
|
||||
### Developer is Blocked
|
||||
1. Check the blocker: `roboco_task_get(task_id)`
|
||||
2. Can you resolve it? → Do so and notify dev
|
||||
2. Can you resolve it? → Do so and unblock: `roboco_task_unblock(task_id)`
|
||||
3. Cross-cell issue? → Escalate: `roboco_escalate()`
|
||||
4. Reassign dev to different task if wait is long
|
||||
|
||||
**IMPORTANT:** When a blocker is resolved, you MUST call `roboco_task_unblock(task_id)`
|
||||
to resume the task. Only PMs can unblock tasks in their cell.
|
||||
|
||||
### Task Needs Clarification
|
||||
1. Document what's unclear
|
||||
2. Ask in #backend-cell or escalate to Main PM
|
||||
@@ -366,16 +374,19 @@ tools:
|
||||
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
||||
- roboco_task_start, roboco_task_plan, roboco_task_progress
|
||||
- roboco_task_create, roboco_task_assign, roboco_task_activate
|
||||
- roboco_task_complete
|
||||
- roboco_task_unblock, roboco_task_complete
|
||||
|
||||
# Session Management (REQUIRED before activation)
|
||||
- roboco_session_create_for_tasks, roboco_session_link_task
|
||||
- roboco_session_unlink_task, roboco_session_get_for_task
|
||||
|
||||
# Journal
|
||||
# Journal (Your Own)
|
||||
- roboco_journal_entry, roboco_journal_decision
|
||||
- roboco_journal_learning, roboco_journal_struggle
|
||||
|
||||
# Team Journals (Read Cell Members + Other PMs)
|
||||
- roboco_journal_read_team, roboco_journal_scope
|
||||
|
||||
# Communication
|
||||
- roboco_message_send, roboco_channel_history
|
||||
|
||||
@@ -413,8 +424,14 @@ permissions:
|
||||
- assign_tasks
|
||||
- change_priority
|
||||
- close_tasks
|
||||
- unblock_tasks
|
||||
- view_all_cell_tasks
|
||||
|
||||
journals_read:
|
||||
- backend cell members (be-dev-1, be-dev-2, be-qa, be-doc)
|
||||
- other cell PMs (fe-pm, ux-pm)
|
||||
- main-pm
|
||||
|
||||
notify_targets:
|
||||
- be-dev-1
|
||||
- be-dev-2
|
||||
|
||||
@@ -52,6 +52,10 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_journal_struggle(data)` - Document a challenge
|
||||
- `roboco_journal_search(query, top_k)` - Search past journal entries
|
||||
|
||||
**Team Journal Access (Verify Developer Work):**
|
||||
- `roboco_journal_read_team(target_agent, entry_type?, task_id?, limit?)` - Read a teammate's journal entries
|
||||
- `roboco_journal_scope()` - See which journals you can access (cell members can read each other's journals)
|
||||
|
||||
**Communication:**
|
||||
- `roboco_channel_list()` - List available channels
|
||||
- `roboco_channel_history(channel_slug, limit?)` - Read channel history
|
||||
@@ -89,15 +93,16 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `handoff_summary` - Summary for reviewers
|
||||
- `progress_updates` - Timestamped progress with percentages
|
||||
- Commits list
|
||||
- **Cell member journals** - You can read journals of your cell members (BE-Dev-1, BE-Dev-2, BE-Documenter)
|
||||
|
||||
**What you CANNOT see:**
|
||||
- Developer's personal journal (journals are private per agent)
|
||||
**Verifying journal-related acceptance criteria:**
|
||||
If criteria mentions journaling (e.g., "journal contains a report"), verify directly:
|
||||
```python
|
||||
# Check if dev created the required journal entry
|
||||
roboco_journal_read_team("be-dev-1", task_id="{task_id}", limit=10)
|
||||
```
|
||||
|
||||
**Journal-related acceptance criteria:**
|
||||
If criteria mentions journaling (e.g., "journal contains a report"), you CANNOT verify this directly. Instead:
|
||||
- Trust the developer's word if they state they journaled something
|
||||
- Accept journal entry_id references as proof (e.g., "Journaled findings in entry #abc123")
|
||||
- Only fail if dev provides NO evidence of journaling when required
|
||||
This returns journal entries filtered by task. Look for the required entry type/content.
|
||||
|
||||
Read all available notes. If dev_notes is empty or unclear, that's a QA FAIL reason.
|
||||
|
||||
@@ -267,11 +272,14 @@ tools:
|
||||
- roboco_task_qa_pass, roboco_task_qa_fail
|
||||
- roboco_task_escalate, roboco_agent_idle
|
||||
|
||||
# Journal
|
||||
# Journal (Your Own)
|
||||
- roboco_journal_entry, roboco_journal_reflect
|
||||
- roboco_journal_decision, roboco_journal_learning
|
||||
- roboco_journal_struggle, roboco_journal_search
|
||||
|
||||
# Team Journals (Read Cell Members)
|
||||
- roboco_journal_read_team, roboco_journal_scope
|
||||
|
||||
# Communication
|
||||
- roboco_channel_list, roboco_channel_history
|
||||
- roboco_message_send, roboco_ask_question
|
||||
@@ -299,6 +307,9 @@ permissions:
|
||||
- qa-all
|
||||
- all-hands
|
||||
|
||||
journals_read:
|
||||
- backend cell members (be-dev-1, be-dev-2, be-doc, be-pm)
|
||||
|
||||
task_permissions:
|
||||
- claim_qa_tasks
|
||||
- qa_pass_tasks
|
||||
|
||||
@@ -48,7 +48,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
**Task Management:**
|
||||
- `roboco_task_scan()` - Check for tasks requiring your attention
|
||||
- `roboco_task_get(task_id)` - Get task details
|
||||
- `roboco_task_create(...)` - Create new tasks for cells (created in BACKLOG status)
|
||||
- `roboco_task_create(...)` - Create new tasks for cells (pass `status: "backlog"` for setup phase)
|
||||
- `roboco_task_activate(task_id)` - Activate task from BACKLOG to PENDING (after session created)
|
||||
|
||||
**Session Management (Cross-Cell Work Sessions):**
|
||||
@@ -128,22 +128,22 @@ Translate Board direction into cell priorities:
|
||||
- Balance workload across cells
|
||||
|
||||
### DISTRIBUTE
|
||||
Push work to cells. Tasks are created with BACKLOG status - they won't be
|
||||
visible to orchestrators until you activate them.
|
||||
Push work to cells. Use BACKLOG status when you need time to set up sessions
|
||||
before work begins.
|
||||
|
||||
**Standard Distribution Workflow:**
|
||||
|
||||
**1. CREATE TASKS (BACKLOG)**
|
||||
Create high-level task records for each cell:
|
||||
**1. CREATE TASKS (with BACKLOG for setup)**
|
||||
Create task records for each cell with explicit BACKLOG status:
|
||||
```python
|
||||
roboco_task_create({
|
||||
"title": "Build preferences API",
|
||||
"description": "GET/PUT /api/v1/users/{id}/preferences",
|
||||
"team": "backend",
|
||||
"acceptance_criteria": ["Endpoint implemented", "Tests passing"],
|
||||
"assigned_to": "be-pm" # Assign to Cell PM for triage
|
||||
"assigned_to": "be-pm",
|
||||
"status": "backlog" # Explicit - gives you time to set up session
|
||||
})
|
||||
# Task created with BACKLOG status
|
||||
```
|
||||
|
||||
**2. CREATE WORK SESSION (REQUIRED)**
|
||||
|
||||
@@ -49,7 +49,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)` - Submit for QA review
|
||||
- `roboco_task_escalate(task_id, reason)` - Escalate issues to PM
|
||||
|
||||
**Journal (Document Your Thinking):**
|
||||
**Journal (Your Own):**
|
||||
- `roboco_journal_entry(data)` - General journal entry
|
||||
- `roboco_journal_reflect(data)` - Task reflection (what done, learned, struggled)
|
||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
||||
@@ -58,6 +58,10 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- `roboco_journal_search(query, top_k)` - Search past journal entries
|
||||
- `roboco_journal_recent(limit)` - Get recent entries
|
||||
|
||||
**Team Journal Access (Read Cell Members):**
|
||||
- `roboco_journal_read_team(target_agent, entry_type?, task_id?, limit?)` - Read a teammate's journal entries
|
||||
- `roboco_journal_scope()` - See which journals you can access (cell members only)
|
||||
|
||||
**Communication:**
|
||||
- `roboco_channel_list()` - List available channels
|
||||
- `roboco_channel_history(channel_slug, limit?)` - Read channel history
|
||||
@@ -156,7 +160,7 @@ roboco_task_pause(task_id, {
|
||||
**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
|
||||
2. **Journal (personal)** - Via `roboco_journal_reflect` - Cell members can read each other's journals
|
||||
|
||||
**Tool:** `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)`
|
||||
|
||||
@@ -173,7 +177,7 @@ roboco_task_submit_qa(task_id, {
|
||||
})
|
||||
```
|
||||
|
||||
**Tool:** `roboco_journal_reflect(data)` (Personal - QA cannot see this)
|
||||
**Tool:** `roboco_journal_reflect(data)` (Cell members can read your journal)
|
||||
|
||||
Document what you did, learned, struggled with for your own growth.
|
||||
|
||||
@@ -285,12 +289,15 @@ tools:
|
||||
- roboco_task_submit_verification, roboco_task_submit_qa
|
||||
- roboco_task_escalate, roboco_agent_idle
|
||||
|
||||
# Journal
|
||||
# Journal (Your Own)
|
||||
- roboco_journal_entry, roboco_journal_reflect
|
||||
- roboco_journal_decision, roboco_journal_learning
|
||||
- roboco_journal_struggle, roboco_journal_search
|
||||
- roboco_journal_recent
|
||||
|
||||
# Team Journals (Read Cell Members)
|
||||
- roboco_journal_read_team, roboco_journal_scope
|
||||
|
||||
# Communication
|
||||
- roboco_channel_list, roboco_channel_history
|
||||
- roboco_message_send, roboco_ask_question
|
||||
@@ -326,4 +333,7 @@ permissions:
|
||||
- update_own_tasks
|
||||
- escalate_tasks
|
||||
- request_qa_review
|
||||
|
||||
journals_read:
|
||||
- frontend cell members (fe-dev-1, fe-dev-2, fe-qa, fe-doc, fe-pm)
|
||||
```
|
||||
|
||||
@@ -44,6 +44,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||
- `roboco_task_create(data)` - Create subtasks for developers
|
||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||
- `roboco_task_unblock(task_id)` - Unblock a blocked task (PM only)
|
||||
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
||||
|
||||
**Session Management (Work Sessions for Tasks):**
|
||||
@@ -52,7 +53,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_session_unlink_task(session_id, task_id)` - Remove task from session
|
||||
- `roboco_session_get_for_task(task_id)` - Get sessions linked to a task
|
||||
|
||||
**Journal (Document Your Thinking):**
|
||||
**Journal (Your Own):**
|
||||
- `roboco_journal_entry(data)` - General journal entry
|
||||
- `roboco_journal_reflect(data)` - Task reflection
|
||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
||||
@@ -60,6 +61,10 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_journal_struggle(data)` - Document a challenge
|
||||
- `roboco_journal_search(query, top_k)` - Search past entries
|
||||
|
||||
**Team Journal Access (Read Cell Members):**
|
||||
- `roboco_journal_read_team(target_agent, entry_type?, task_id?, limit?)` - Read a teammate's journal entries
|
||||
- `roboco_journal_scope()` - See which journals you can access (cell members, other PMs, Main PM)
|
||||
|
||||
**Communication:**
|
||||
- `roboco_channel_list()` - List available channels
|
||||
- `roboco_channel_history(channel_slug)` - Read channel history
|
||||
@@ -155,7 +160,7 @@ roboco_task_assign("{task_id}", "fe-dev-1")
|
||||
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
||||
- Do NOT keep tasks for yourself - delegate to developers!
|
||||
|
||||
### 7.5. CREATE WORK SESSION (REQUIRED)
|
||||
### 7a. CREATE WORK SESSION (REQUIRED)
|
||||
**Tool:** `roboco_session_create_for_tasks(data)`
|
||||
|
||||
After delegating, you MUST create a work session for the task:
|
||||
@@ -178,7 +183,7 @@ roboco_session_create_for_tasks({
|
||||
- QA and documenter see full context when reviewing
|
||||
- Subtasks auto-inherit parent task's primary session
|
||||
|
||||
### 7.6. ACTIVATE TASK (REQUIRED)
|
||||
### 7b. ACTIVATE TASK (REQUIRED)
|
||||
**Tool:** `roboco_task_activate(task_id)`
|
||||
|
||||
After creating the session, activate the task:
|
||||
@@ -186,9 +191,9 @@ After creating the session, activate the task:
|
||||
roboco_task_activate("task-uuid")
|
||||
```
|
||||
|
||||
**Task flow:**
|
||||
**Task flow (when using backlog for setup):**
|
||||
```
|
||||
CREATE (backlog) → SESSION → ACTIVATE (pending) → Orchestrator spawns dev
|
||||
CREATE (status: backlog) → SESSION → ACTIVATE (pending) → Orchestrator spawns dev
|
||||
```
|
||||
|
||||
### 8. COMMUNICATE
|
||||
@@ -277,13 +282,16 @@ Can these be added?
|
||||
1. Confirm exact API need (endpoint, schema)
|
||||
2. Contact BE-PM with specific ask
|
||||
3. If long wait: have dev use mock data
|
||||
4. Track unblock and notify dev when ready
|
||||
4. When resolved: `roboco_task_unblock(task_id)` to resume
|
||||
|
||||
### Developer is Blocked on Design
|
||||
1. Confirm what's missing (states, specs, assets)
|
||||
2. Contact UX-PM with specific ask
|
||||
3. If minor: can dev proceed with best judgment?
|
||||
4. If major: wait for design or escalate
|
||||
4. When resolved: `roboco_task_unblock(task_id)` to resume
|
||||
|
||||
**IMPORTANT:** When a blocker is resolved, you MUST call `roboco_task_unblock(task_id)`
|
||||
to resume the task. Only PMs can unblock tasks in their cell.
|
||||
|
||||
### All Subtasks Complete
|
||||
1. Review parent task: `roboco_task_get(parent_id)`
|
||||
@@ -367,16 +375,19 @@ tools:
|
||||
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
||||
- roboco_task_start, roboco_task_plan, roboco_task_progress
|
||||
- roboco_task_create, roboco_task_assign, roboco_task_activate
|
||||
- roboco_task_complete
|
||||
- roboco_task_unblock, roboco_task_complete
|
||||
|
||||
# Session Management (REQUIRED before activation)
|
||||
- roboco_session_create_for_tasks, roboco_session_link_task
|
||||
- roboco_session_unlink_task, roboco_session_get_for_task
|
||||
|
||||
# Journal
|
||||
# Journal (Your Own)
|
||||
- roboco_journal_entry, roboco_journal_decision
|
||||
- roboco_journal_learning, roboco_journal_struggle
|
||||
|
||||
# Team Journals (Read Cell Members + Other PMs)
|
||||
- roboco_journal_read_team, roboco_journal_scope
|
||||
|
||||
# Communication
|
||||
- roboco_message_send, roboco_channel_history
|
||||
|
||||
@@ -414,8 +425,14 @@ permissions:
|
||||
- assign_tasks
|
||||
- change_priority
|
||||
- close_tasks
|
||||
- unblock_tasks
|
||||
- view_all_cell_tasks
|
||||
|
||||
journals_read:
|
||||
- frontend cell members (fe-dev-1, fe-dev-2, fe-qa, fe-doc)
|
||||
- other cell PMs (be-pm, ux-pm)
|
||||
- main-pm
|
||||
|
||||
notify_targets:
|
||||
- fe-dev-1
|
||||
- fe-dev-2
|
||||
|
||||
@@ -41,13 +41,17 @@ You are the Frontend QA Engineer at RoboCo, an AI-powered software company. You
|
||||
- `roboco_task_qa_fail(task_id, qa_notes, issues)` - Reject with issues
|
||||
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
||||
|
||||
**Journal:**
|
||||
**Journal (Your Own):**
|
||||
- `roboco_journal_entry(data)` - General journal entry
|
||||
- `roboco_journal_reflect(data)` - Task reflection
|
||||
- `roboco_journal_decision(data)` - Log decisions
|
||||
- `roboco_journal_learning(data)` - Document learnings
|
||||
- `roboco_journal_struggle(data)` - Document challenges
|
||||
|
||||
**Team Journal Access (Verify Developer Work):**
|
||||
- `roboco_journal_read_team(target_agent, entry_type?, task_id?, limit?)` - Read a teammate's journal entries
|
||||
- `roboco_journal_scope()` - See which journals you can access
|
||||
|
||||
**Communication:**
|
||||
- `roboco_channel_list()` - List channels
|
||||
- `roboco_channel_history(channel_slug)` - Read history
|
||||
@@ -78,15 +82,13 @@ If none: `roboco_agent_idle()`
|
||||
- `dev_notes` - Developer's work evidence
|
||||
- `progress_updates` - Timestamped progress with percentages
|
||||
- Design specs and acceptance criteria
|
||||
- **Cell member journals** - You can read journals of FE-Dev-1, FE-Dev-2, FE-Documenter
|
||||
|
||||
**What you CANNOT see:**
|
||||
- Developer's personal journal (private)
|
||||
|
||||
**Journal-related acceptance criteria:**
|
||||
If criteria mentions journaling, you CANNOT verify this directly. Instead:
|
||||
- Trust the developer's word if they state they journaled something
|
||||
- Accept journal entry_id references as proof
|
||||
- Only fail if dev provides NO evidence of journaling when required
|
||||
**Verifying journal-related acceptance criteria:**
|
||||
If criteria mentions journaling (e.g., "journal contains a report"), verify directly:
|
||||
```python
|
||||
roboco_journal_read_team("fe-dev-1", task_id="{task_id}", limit=10)
|
||||
```
|
||||
|
||||
If dev_notes is empty, that's a valid FAIL reason.
|
||||
|
||||
@@ -156,12 +158,17 @@ capabilities:
|
||||
- journaling
|
||||
|
||||
tools:
|
||||
# Task Management
|
||||
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
||||
- roboco_task_start, roboco_task_progress
|
||||
- roboco_task_qa_pass, roboco_task_qa_fail
|
||||
- roboco_task_escalate, roboco_agent_idle
|
||||
# Journal (Your Own)
|
||||
- roboco_journal_entry, roboco_journal_reflect
|
||||
- roboco_journal_decision, roboco_journal_learning
|
||||
# Team Journals (Read Cell Members)
|
||||
- roboco_journal_read_team, roboco_journal_scope
|
||||
# Communication
|
||||
- roboco_channel_list, roboco_channel_history
|
||||
- roboco_message_send, roboco_ask_question
|
||||
```
|
||||
@@ -183,6 +190,9 @@ permissions:
|
||||
- qa-all
|
||||
- all-hands
|
||||
|
||||
journals_read:
|
||||
- frontend cell members (fe-dev-1, fe-dev-2, fe-doc, fe-pm)
|
||||
|
||||
task_permissions:
|
||||
- claim_qa_tasks
|
||||
- qa_pass_tasks
|
||||
|
||||
@@ -50,7 +50,7 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)` - Submit for QA review
|
||||
- `roboco_task_escalate(task_id, reason)` - Escalate issues to PM
|
||||
|
||||
**Journal (Document Your Thinking):**
|
||||
**Journal (Your Own):**
|
||||
- `roboco_journal_entry(data)` - General journal entry
|
||||
- `roboco_journal_reflect(data)` - Task reflection (what done, learned, struggled)
|
||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
||||
@@ -59,6 +59,10 @@ You interact with RoboCo systems through MCP tools. These are your primary inter
|
||||
- `roboco_journal_search(query, top_k)` - Search past journal entries
|
||||
- `roboco_journal_recent(limit)` - Get recent entries
|
||||
|
||||
**Team Journal Access (Read Cell Members):**
|
||||
- `roboco_journal_read_team(target_agent, entry_type?, task_id?, limit?)` - Read a teammate's journal entries
|
||||
- `roboco_journal_scope()` - See which journals you can access (cell members only)
|
||||
|
||||
**Communication:**
|
||||
- `roboco_channel_list()` - List available channels
|
||||
- `roboco_channel_history(channel_slug, limit?)` - Read channel history
|
||||
@@ -155,7 +159,7 @@ Checklist:
|
||||
**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
|
||||
2. **Journal (personal)** - Via `roboco_journal_reflect` - Cell members can read each other's journals
|
||||
|
||||
**Tool:** `roboco_task_submit_qa(task_id, dev_notes, handoff_summary)`
|
||||
|
||||
@@ -172,7 +176,7 @@ roboco_task_submit_qa(task_id, {
|
||||
})
|
||||
```
|
||||
|
||||
**Tool:** `roboco_journal_reflect(data)` (Personal - QA cannot see this)
|
||||
**Tool:** `roboco_journal_reflect(data)` (Cell members can read your journal)
|
||||
|
||||
Document what you designed, decisions made, what you learned for your own growth.
|
||||
|
||||
@@ -255,12 +259,15 @@ tools:
|
||||
- roboco_task_submit_verification, roboco_task_submit_qa
|
||||
- roboco_task_escalate, roboco_agent_idle
|
||||
|
||||
# Journal
|
||||
# Journal (Your Own)
|
||||
- roboco_journal_entry, roboco_journal_reflect
|
||||
- roboco_journal_decision, roboco_journal_learning
|
||||
- roboco_journal_struggle, roboco_journal_search
|
||||
- roboco_journal_recent
|
||||
|
||||
# Team Journals (Read Cell Members)
|
||||
- roboco_journal_read_team, roboco_journal_scope
|
||||
|
||||
# Communication
|
||||
- roboco_channel_list, roboco_channel_history
|
||||
- roboco_message_send, roboco_ask_question
|
||||
@@ -293,4 +300,7 @@ permissions:
|
||||
- update_own_tasks
|
||||
- escalate_tasks
|
||||
- request_qa_review
|
||||
|
||||
journals_read:
|
||||
- ux_ui cell members (ux-dev, ux-qa, ux-doc, ux-pm)
|
||||
```
|
||||
|
||||
@@ -44,6 +44,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_task_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||
- `roboco_task_create(data)` - Create subtasks for designers
|
||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||
- `roboco_task_unblock(task_id)` - Unblock a blocked task (PM only)
|
||||
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
||||
|
||||
**Session Management (Work Sessions for Tasks):**
|
||||
@@ -52,7 +53,7 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_session_unlink_task(session_id, task_id)` - Remove task from session
|
||||
- `roboco_session_get_for_task(task_id)` - Get sessions linked to a task
|
||||
|
||||
**Journal (Document Your Thinking):**
|
||||
**Journal (Your Own):**
|
||||
- `roboco_journal_entry(data)` - General journal entry
|
||||
- `roboco_journal_reflect(data)` - Task reflection
|
||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
||||
@@ -60,6 +61,10 @@ You interact with RoboCo systems through MCP tools:
|
||||
- `roboco_journal_struggle(data)` - Document a challenge
|
||||
- `roboco_journal_search(query, top_k)` - Search past entries
|
||||
|
||||
**Team Journal Access (Read Cell Members):**
|
||||
- `roboco_journal_read_team(target_agent, entry_type?, task_id?, limit?)` - Read a teammate's journal entries
|
||||
- `roboco_journal_scope()` - See which journals you can access (cell members, other PMs, Main PM)
|
||||
|
||||
**Communication:**
|
||||
- `roboco_channel_list()` - List available channels
|
||||
- `roboco_channel_history(channel_slug)` - Read channel history
|
||||
@@ -155,7 +160,7 @@ roboco_task_assign("{task_id}", "ux-dev")
|
||||
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
||||
- Do NOT keep tasks for yourself - delegate to designers!
|
||||
|
||||
### 7.5. CREATE WORK SESSION (REQUIRED)
|
||||
### 7a. CREATE WORK SESSION (REQUIRED)
|
||||
**Tool:** `roboco_session_create_for_tasks(data)`
|
||||
|
||||
After delegating, you MUST create a work session for the task:
|
||||
@@ -178,7 +183,7 @@ roboco_session_create_for_tasks({
|
||||
- QA and documenter see design context
|
||||
- Frontend can review design discussion history
|
||||
|
||||
### 7.6. ACTIVATE TASK (REQUIRED)
|
||||
### 7b. ACTIVATE TASK (REQUIRED)
|
||||
**Tool:** `roboco_task_activate(task_id)`
|
||||
|
||||
After creating the session, activate the task:
|
||||
@@ -188,7 +193,7 @@ roboco_task_activate("task-uuid")
|
||||
|
||||
**Task flow:**
|
||||
```
|
||||
CREATE (backlog) → SESSION → ACTIVATE (pending) → Orchestrator spawns dev
|
||||
CREATE (status: backlog) → SESSION → ACTIVATE (pending) → Orchestrator spawns dev
|
||||
```
|
||||
|
||||
### 8. COMMUNICATE
|
||||
@@ -279,6 +284,15 @@ UX-PM: @ProductOwner Question on TASK-055:
|
||||
3. Assess UX-Dev workload - can they pivot?
|
||||
4. Communicate realistic timeline to FE-PM
|
||||
|
||||
### Designer is Blocked
|
||||
1. Check the blocker: `roboco_task_get(task_id)`
|
||||
2. Can you resolve it? → Do so and unblock: `roboco_task_unblock(task_id)`
|
||||
3. Requirements issue? → Escalate to Product Owner
|
||||
4. Reassign designer to different task if wait is long
|
||||
|
||||
**IMPORTANT:** When a blocker is resolved, you MUST call `roboco_task_unblock(task_id)`
|
||||
to resume the task. Only PMs can unblock tasks in their cell.
|
||||
|
||||
### Design Requirements Unclear
|
||||
1. Document specific questions
|
||||
2. Escalate to Product Owner with specific asks
|
||||
@@ -367,16 +381,19 @@ tools:
|
||||
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
||||
- roboco_task_start, roboco_task_plan, roboco_task_progress
|
||||
- roboco_task_create, roboco_task_assign, roboco_task_activate
|
||||
- roboco_task_complete
|
||||
- roboco_task_unblock, roboco_task_complete
|
||||
|
||||
# Session Management (REQUIRED before activation)
|
||||
- roboco_session_create_for_tasks, roboco_session_link_task
|
||||
- roboco_session_unlink_task, roboco_session_get_for_task
|
||||
|
||||
# Journal
|
||||
# Journal (Your Own)
|
||||
- roboco_journal_entry, roboco_journal_decision
|
||||
- roboco_journal_learning, roboco_journal_struggle
|
||||
|
||||
# Team Journals (Read Cell Members + Other PMs)
|
||||
- roboco_journal_read_team, roboco_journal_scope
|
||||
|
||||
# Communication
|
||||
- roboco_message_send, roboco_channel_history
|
||||
|
||||
@@ -414,8 +431,14 @@ permissions:
|
||||
- assign_tasks
|
||||
- change_priority
|
||||
- close_tasks
|
||||
- unblock_tasks
|
||||
- view_all_cell_tasks
|
||||
|
||||
journals_read:
|
||||
- ux_ui cell members (ux-dev, ux-qa, ux-doc)
|
||||
- other cell PMs (be-pm, fe-pm)
|
||||
- main-pm
|
||||
|
||||
notify_targets:
|
||||
- ux-dev
|
||||
- ux-qa
|
||||
|
||||
@@ -42,12 +42,16 @@ You are the UX/UI QA Engineer at RoboCo, an AI-powered software company. You ens
|
||||
- `roboco_task_qa_fail(task_id, qa_notes, issues)` - Reject with issues
|
||||
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
||||
|
||||
**Journal:**
|
||||
**Journal (Your Own):**
|
||||
- `roboco_journal_entry(data)` - General journal entry
|
||||
- `roboco_journal_reflect(data)` - Task reflection
|
||||
- `roboco_journal_decision(data)` - Log decisions
|
||||
- `roboco_journal_learning(data)` - Document learnings
|
||||
|
||||
**Team Journal Access (Verify Designer Work):**
|
||||
- `roboco_journal_read_team(target_agent, entry_type?, task_id?, limit?)` - Read a teammate's journal entries
|
||||
- `roboco_journal_scope()` - See which journals you can access
|
||||
|
||||
**Communication:**
|
||||
- `roboco_channel_list()` - List channels
|
||||
- `roboco_channel_history(channel_slug)` - Read history
|
||||
@@ -78,15 +82,13 @@ If none: `roboco_agent_idle()`
|
||||
- `dev_notes` - Designer's work evidence and Figma links
|
||||
- `progress_updates` - Timestamped progress with percentages
|
||||
- Requirements and acceptance criteria
|
||||
- **Cell member journals** - You can read journals of UX-Dev, UX-Documenter
|
||||
|
||||
**What you CANNOT see:**
|
||||
- Designer's personal journal (private)
|
||||
|
||||
**Journal-related acceptance criteria:**
|
||||
If criteria mentions journaling, you CANNOT verify this directly. Instead:
|
||||
- Trust the designer's word if they state they journaled something
|
||||
- Accept journal entry_id references as proof
|
||||
- Only fail if designer provides NO evidence of journaling when required
|
||||
**Verifying journal-related acceptance criteria:**
|
||||
If criteria mentions journaling (e.g., "journal contains design rationale"), verify directly:
|
||||
```python
|
||||
roboco_journal_read_team("ux-dev", task_id="{task_id}", limit=10)
|
||||
```
|
||||
|
||||
If dev_notes is empty or no Figma link provided, that's a valid FAIL reason.
|
||||
|
||||
@@ -154,12 +156,17 @@ capabilities:
|
||||
- journaling
|
||||
|
||||
tools:
|
||||
# Task Management
|
||||
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
||||
- roboco_task_start, roboco_task_progress
|
||||
- roboco_task_qa_pass, roboco_task_qa_fail
|
||||
- roboco_task_escalate, roboco_agent_idle
|
||||
# Journal (Your Own)
|
||||
- roboco_journal_entry, roboco_journal_reflect
|
||||
- roboco_journal_decision, roboco_journal_learning
|
||||
# Team Journals (Read Cell Members)
|
||||
- roboco_journal_read_team, roboco_journal_scope
|
||||
# Communication
|
||||
- roboco_channel_list, roboco_channel_history
|
||||
- roboco_message_send, roboco_ask_question
|
||||
```
|
||||
@@ -181,6 +188,9 @@ permissions:
|
||||
- qa-all
|
||||
- all-hands
|
||||
|
||||
journals_read:
|
||||
- uxui cell members (ux-dev, ux-doc, ux-pm)
|
||||
|
||||
task_permissions:
|
||||
- claim_qa_tasks
|
||||
- qa_pass_tasks
|
||||
|
||||
@@ -91,6 +91,7 @@ def upgrade() -> None:
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"backlog",
|
||||
"pending",
|
||||
"claimed",
|
||||
"in_progress",
|
||||
|
||||
@@ -36,6 +36,9 @@ COPY --chown=agent:agent pyproject.toml uv.lock README.md /app/
|
||||
USER agent
|
||||
|
||||
# Install Python dependencies for MCP servers (as agent)
|
||||
# Increase timeout for large NVIDIA packages (674MB cudnn, 858MB torch)
|
||||
ENV UV_HTTP_TIMEOUT=300
|
||||
ENV UV_CONCURRENT_DOWNLOADS=4
|
||||
RUN uv python install 3.13 && uv sync --frozen --python 3.13
|
||||
|
||||
# Claude Code will use mounted ~/.claude for auth
|
||||
|
||||
@@ -37,6 +37,9 @@ COPY pyproject.toml uv.lock alembic.ini README.md /app/
|
||||
COPY alembic /app/alembic
|
||||
|
||||
# Install Python dependencies
|
||||
# Increase timeout for large NVIDIA packages (674MB cudnn, 858MB torch)
|
||||
ENV UV_HTTP_TIMEOUT=300
|
||||
ENV UV_CONCURRENT_DOWNLOADS=4
|
||||
RUN uv python install 3.13 && uv sync --frozen --python 3.13
|
||||
|
||||
# Expose API port
|
||||
|
||||
@@ -556,6 +556,50 @@ class Agent(ABC):
|
||||
self.log.warning("Failed to read dev notes", error=str(e))
|
||||
return "Dev notes unavailable"
|
||||
|
||||
async def _read_team_journal_for_task(self, task_id: UUID) -> str:
|
||||
"""
|
||||
Read team member journal entries for a specific task.
|
||||
|
||||
Cell members can read each other's journals. This queries for
|
||||
journal entries linked to the given task.
|
||||
|
||||
Args:
|
||||
task_id: Task to get journal entries for
|
||||
|
||||
Returns:
|
||||
Formatted journal entries or empty string if none/error
|
||||
"""
|
||||
try:
|
||||
# Get task to find assigned developer
|
||||
task = await self._api_call("GET", f"/tasks/{task_id}")
|
||||
assigned_to = task.get("assigned_to")
|
||||
if not assigned_to:
|
||||
return ""
|
||||
|
||||
# Query journal entries for this task from the assigned agent
|
||||
result = await self._api_call(
|
||||
"GET",
|
||||
f"/journals/{assigned_to}/entries",
|
||||
params={"task_id": str(task_id), "limit": 10},
|
||||
)
|
||||
entries = result.get("items", [])
|
||||
if not entries:
|
||||
return ""
|
||||
|
||||
# Format entries
|
||||
formatted = []
|
||||
for entry in entries:
|
||||
entry_type = entry.get("entry_type", "entry")
|
||||
title = entry.get("title", "Untitled")
|
||||
content = entry.get("content", "")
|
||||
timestamp = entry.get("created_at", "")
|
||||
formatted.append(f"[{timestamp}] {entry_type}: {title}\n{content}")
|
||||
|
||||
return "\n\n".join(formatted)
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to read team journal", error=str(e))
|
||||
return ""
|
||||
|
||||
async def _get_task_commits(self, task_id: UUID) -> list[str]:
|
||||
"""Get commits for the task."""
|
||||
try:
|
||||
@@ -596,6 +640,26 @@ class Agent(ABC):
|
||||
"""Mark task as blocked."""
|
||||
await self._update_task_status(task_id, TaskStatus.BLOCKED)
|
||||
|
||||
async def _unblock_task(self, task_id: UUID) -> bool:
|
||||
"""
|
||||
Unblock a blocked task.
|
||||
|
||||
Only PMs can unblock tasks in their cell.
|
||||
|
||||
Args:
|
||||
task_id: Task to unblock
|
||||
|
||||
Returns:
|
||||
True if unblocked successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
await self._api_call("POST", f"/tasks/{task_id}/unblock")
|
||||
self.log.info("Task unblocked", task_id=str(task_id))
|
||||
return True
|
||||
except Exception as e:
|
||||
self.log.error("Failed to unblock task", task_id=str(task_id), error=str(e))
|
||||
return False
|
||||
|
||||
async def _mark_awaiting_qa(self, task_id: UUID) -> None:
|
||||
"""Mark task as awaiting QA review."""
|
||||
await self._update_task_status(task_id, TaskStatus.AWAITING_QA)
|
||||
|
||||
@@ -210,9 +210,23 @@ medium,TASK-abc123,P1,backend-dev-1
|
||||
- Answer questions
|
||||
- Clarify requirements
|
||||
- Remove small blockers
|
||||
- Unblock blocked tasks when blocker is resolved
|
||||
"""
|
||||
self.log.debug("FACILITATE phase")
|
||||
|
||||
# Check for blocked tasks that may be resolvable
|
||||
blocked_tasks = await self._get_blocked_tasks()
|
||||
for task_id in blocked_tasks:
|
||||
resolved = await self._check_blocker_resolved(task_id)
|
||||
if resolved:
|
||||
success = await self._unblock_task(task_id)
|
||||
if success:
|
||||
await self.send_message(
|
||||
self._cell_channel_id or self.id,
|
||||
f"TASK-{str(task_id)[:8]} unblocked - blocker resolved",
|
||||
message_type="action",
|
||||
)
|
||||
|
||||
# Check for pending questions in channel
|
||||
questions = await self._get_pending_questions()
|
||||
|
||||
@@ -454,6 +468,47 @@ Please review and provide guidance.
|
||||
self.log.warning("Failed to get active tasks", error=str(e))
|
||||
return []
|
||||
|
||||
async def _get_blocked_tasks(self) -> list[UUID]:
|
||||
"""Get all blocked tasks in cell."""
|
||||
try:
|
||||
team_param = self.team.value if self.team else None
|
||||
result = await self._api_call(
|
||||
"GET",
|
||||
"/tasks",
|
||||
params={"status": "blocked", "team": team_param},
|
||||
)
|
||||
return [UUID(t["id"]) for t in result.get("items", [])]
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to get blocked tasks", error=str(e))
|
||||
return []
|
||||
|
||||
async def _check_blocker_resolved(self, task_id: UUID) -> bool:
|
||||
"""
|
||||
Check if a task's blocker has been resolved.
|
||||
|
||||
This examines the blocker_reason and checks if conditions are met.
|
||||
For subtask blockers, checks if all subtasks are complete.
|
||||
"""
|
||||
try:
|
||||
result = await self._api_call("GET", f"/tasks/{task_id}")
|
||||
blocker_reason = result.get("blocker_reason", "")
|
||||
|
||||
# Check if subtasks are complete (common blocker)
|
||||
subtasks = result.get("subtasks", [])
|
||||
if subtasks:
|
||||
all_complete = all(
|
||||
s.get("status") == "completed" for s in subtasks
|
||||
)
|
||||
if all_complete:
|
||||
return True
|
||||
|
||||
# If no specific logic, return False (needs manual review)
|
||||
return not blocker_reason # Resolved if reason was cleared
|
||||
|
||||
except Exception as e:
|
||||
self.log.warning("Failed to check blocker", error=str(e))
|
||||
return False
|
||||
|
||||
async def _check_task_progress(self, task_id: UUID) -> dict[str, Any]:
|
||||
"""
|
||||
Check progress of a task.
|
||||
|
||||
+12
-10
@@ -196,10 +196,7 @@ class QAAgent(Agent, PhaseEngine[QATaskPhase, ReviewContext]):
|
||||
- Read dev's handoff notes (from task's dev_notes field)
|
||||
- Review commits
|
||||
- Check conversation history
|
||||
|
||||
NOTE: Journals are PRIVATE. If acceptance criteria mentions journal entries,
|
||||
trust the developer's word. If they reference a journal entry_id, accept
|
||||
that as proof without attempting to read the private content.
|
||||
- Read developer's journal entries for this task (if needed)
|
||||
"""
|
||||
self.log.info("UNDERSTAND phase", task_id=str(ctx.task_id))
|
||||
|
||||
@@ -208,14 +205,22 @@ class QAAgent(Agent, PhaseEngine[QATaskPhase, ReviewContext]):
|
||||
dev_notes = await self._read_dev_notes(ctx.task_id)
|
||||
commits = await self._get_task_commits_formatted(ctx.task_id)
|
||||
|
||||
# Read developer journal entries for this task (cell members can read)
|
||||
dev_journal = await self._read_team_journal_for_task(ctx.task_id)
|
||||
|
||||
# Use TOON for token-efficient context encoding
|
||||
task_context = self._format_review_context(
|
||||
ctx.title, requirements, dev_notes, commits
|
||||
)
|
||||
|
||||
# Include journal context if available
|
||||
journal_context = ""
|
||||
if dev_journal:
|
||||
journal_context = f"\n\nDeveloper Journal Entries:\n{dev_journal}"
|
||||
|
||||
prompt = f"""You are a QA engineer reviewing a completed task.
|
||||
|
||||
{task_context}
|
||||
{task_context}{journal_context}
|
||||
|
||||
Based on this, create test cases to verify the implementation.
|
||||
|
||||
@@ -225,11 +230,8 @@ Focus on:
|
||||
- Integration points
|
||||
- Error handling
|
||||
|
||||
IMPORTANT: Journals are PRIVATE and personal to each agent. You cannot read
|
||||
another agent's journal entries. If acceptance criteria mentions journaling:
|
||||
- Trust the developer's word if they say they journaled something
|
||||
- Accept journal entry_id references as proof (you don't need to verify content)
|
||||
- Only verify the deliverable outputs, not private reflection logs
|
||||
If acceptance criteria mentions journaling requirements, verify them against
|
||||
the developer journal entries provided above.
|
||||
|
||||
Format response as TOON tabular:
|
||||
[N,]{{name,description,steps,expected}}:
|
||||
|
||||
+20
-5
@@ -7,6 +7,19 @@ All enforcement modules and MCP servers should import from here.
|
||||
|
||||
from typing import Final
|
||||
|
||||
from roboco.seeds.initial_data import AGENT_UUIDS
|
||||
|
||||
# Reverse mapping: UUID -> slug (computed from seeds)
|
||||
_UUID_TO_SLUG: Final[dict[str, str]] = {
|
||||
uuid: slug for slug, uuid in AGENT_UUIDS.items()
|
||||
}
|
||||
|
||||
|
||||
def _resolve_to_slug(agent_id: str) -> str:
|
||||
"""Resolve agent ID (UUID or slug) to slug."""
|
||||
return _UUID_TO_SLUG.get(agent_id, agent_id)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AGENT ROLE MAPPINGS
|
||||
# =============================================================================
|
||||
@@ -120,17 +133,19 @@ ESCALATION_CHAIN: Final[dict[str, str]] = {
|
||||
|
||||
|
||||
def get_agent_role(agent_id: str) -> str:
|
||||
"""Get the role for an agent."""
|
||||
return AGENT_ROLE_MAP.get(agent_id, "unknown")
|
||||
"""Get the role for an agent. Accepts both UUID and slug."""
|
||||
slug = _resolve_to_slug(agent_id)
|
||||
return AGENT_ROLE_MAP.get(slug, "unknown")
|
||||
|
||||
|
||||
def get_agent_team(agent_id: str) -> str | None:
|
||||
"""Get the team for an agent."""
|
||||
return AGENT_TEAM_MAP.get(agent_id)
|
||||
"""Get the team for an agent. Accepts both UUID and slug."""
|
||||
slug = _resolve_to_slug(agent_id)
|
||||
return AGENT_TEAM_MAP.get(slug)
|
||||
|
||||
|
||||
def get_agent_cell(agent_id: str) -> str | None:
|
||||
"""Get the cell an agent belongs to (alias for get_agent_team)."""
|
||||
"""Get the cell an agent belongs to. Accepts both UUID and slug."""
|
||||
return get_agent_team(agent_id)
|
||||
|
||||
|
||||
|
||||
+22
-3
@@ -17,7 +17,7 @@ from roboco.db.base import get_db
|
||||
from roboco.models import AgentRole, Team
|
||||
from roboco.runtime import AgentOrchestrator
|
||||
from roboco.services.permissions import AgentContext, PermissionService
|
||||
from roboco.services.repositories import resolve_agent_uuid
|
||||
from roboco.services.repositories import resolve_agent_identity, resolve_agent_uuid
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Coroutine
|
||||
@@ -173,8 +173,26 @@ async def get_agent_context(
|
||||
detail="Missing X-Agent-Role header",
|
||||
)
|
||||
|
||||
# Resolve agent ID (UUID or slug)
|
||||
agent_id = await resolve_agent_id(x_agent_id, db)
|
||||
# Special case: system role (orchestrator) uses well-known UUID
|
||||
# that doesn't exist in the database - bypass DB lookup
|
||||
if x_agent_role.lower() == "system":
|
||||
try:
|
||||
agent_id = UUID(x_agent_id)
|
||||
slug = "system"
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid system agent UUID: {x_agent_id}",
|
||||
) from e
|
||||
else:
|
||||
# Resolve agent ID and slug from database
|
||||
identity = await resolve_agent_identity(db, x_agent_id)
|
||||
if identity is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Agent not found: {x_agent_id}",
|
||||
)
|
||||
agent_id, slug = identity
|
||||
|
||||
try:
|
||||
role = AgentRole(x_agent_role.lower())
|
||||
@@ -193,6 +211,7 @@ async def get_agent_context(
|
||||
agent_id=agent_id,
|
||||
role=role,
|
||||
team=team,
|
||||
slug=slug,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ from roboco.api.schemas.journals import (
|
||||
StruggleRequest,
|
||||
TaskReflectionRequest,
|
||||
)
|
||||
from roboco.models.base import AgentRole, JournalEntryType
|
||||
from roboco.enforcement import JournalAccessDeniedError, validate_journal_access
|
||||
from roboco.models.base import JournalEntryType
|
||||
from roboco.models.journal import (
|
||||
DecisionLogParams,
|
||||
GeneralEntryParams,
|
||||
@@ -513,17 +514,30 @@ async def get_entry(
|
||||
detail=f"Entry not found: {entry_id}",
|
||||
)
|
||||
|
||||
# Check privacy (simplified - in production would check journal ownership)
|
||||
if entry.is_private:
|
||||
journal = await service.get_journal(entry.journal_id)
|
||||
# Allow CEO and Auditor to see private entries
|
||||
is_other_agent = journal and journal.agent_id != agent.agent_id
|
||||
is_unprivileged = agent.role not in [AgentRole.CEO, AgentRole.AUDITOR]
|
||||
if is_other_agent and is_unprivileged:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="This entry is private",
|
||||
)
|
||||
# Get journal to check ownership
|
||||
journal = await service.get_journal(entry.journal_id)
|
||||
if not journal:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Journal not found for entry: {entry_id}",
|
||||
)
|
||||
|
||||
# Get owner's slug for permission checking
|
||||
owner_slug = await service.get_agent_slug(journal.agent_id)
|
||||
if not owner_slug:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Journal owner not found",
|
||||
)
|
||||
|
||||
# Check permission (cell members can see all entries including private)
|
||||
try:
|
||||
validate_journal_access(agent.slug or "", owner_slug)
|
||||
except JournalAccessDeniedError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=e.message,
|
||||
) from e
|
||||
|
||||
return JournalEntryResponse(
|
||||
id=entry.id,
|
||||
@@ -579,13 +593,13 @@ async def delete_entry(
|
||||
@router.get("/{agent_id}", response_model=JournalResponse)
|
||||
async def get_journal_by_agent(
|
||||
agent_id: str,
|
||||
_agent: CurrentAgentContext,
|
||||
agent: CurrentAgentContext,
|
||||
db: DbSession,
|
||||
) -> JournalResponse:
|
||||
"""
|
||||
Get a journal by agent ID (UUID) or slug (e.g., "be-dev-1").
|
||||
|
||||
Note: Access may be restricted based on privacy settings.
|
||||
Access is restricted based on cell membership and role hierarchy.
|
||||
"""
|
||||
service = get_journal_service(db)
|
||||
|
||||
@@ -597,6 +611,23 @@ async def get_journal_by_agent(
|
||||
detail=f"Agent not found: {agent_id}",
|
||||
)
|
||||
|
||||
# Get target agent's slug for permission checking
|
||||
target_slug = await service.get_agent_slug(resolved_id)
|
||||
if not target_slug:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Agent not found: {agent_id}",
|
||||
)
|
||||
|
||||
# Check permission
|
||||
try:
|
||||
validate_journal_access(agent.slug or "", target_slug)
|
||||
except JournalAccessDeniedError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=e.message,
|
||||
) from e
|
||||
|
||||
journal = await service.get_journal_by_agent(resolved_id)
|
||||
if not journal:
|
||||
raise HTTPException(
|
||||
@@ -620,14 +651,15 @@ async def get_journal_by_agent(
|
||||
@router.get("/{agent_id}/entries", response_model=list[JournalEntryResponse])
|
||||
async def list_agent_entries(
|
||||
agent_id: str,
|
||||
_agent: CurrentAgentContext,
|
||||
agent: CurrentAgentContext,
|
||||
db: DbSession,
|
||||
params: Annotated[ListEntriesParams, Depends()],
|
||||
) -> list[JournalEntryResponse]:
|
||||
"""
|
||||
List journal entries for a specific agent by UUID or slug.
|
||||
|
||||
CEO/Auditor can use this to monitor agent journals.
|
||||
Access is restricted based on cell membership and role hierarchy.
|
||||
Cell members can see all entries (including private) from each other.
|
||||
"""
|
||||
service = get_journal_service(db)
|
||||
|
||||
@@ -639,6 +671,23 @@ async def list_agent_entries(
|
||||
detail=f"Agent not found: {agent_id}",
|
||||
)
|
||||
|
||||
# Get target agent's slug for permission checking
|
||||
target_slug = await service.get_agent_slug(resolved_id)
|
||||
if not target_slug:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Agent not found: {agent_id}",
|
||||
)
|
||||
|
||||
# Check permission
|
||||
try:
|
||||
validate_journal_access(agent.slug or "", target_slug)
|
||||
except JournalAccessDeniedError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=e.message,
|
||||
) from e
|
||||
|
||||
journal = await service.get_journal_by_agent(resolved_id)
|
||||
if not journal:
|
||||
return []
|
||||
@@ -653,6 +702,7 @@ async def list_agent_entries(
|
||||
detail=f"Invalid entry type: {e}",
|
||||
) from e
|
||||
|
||||
# Cell members with access can see all entries including private
|
||||
entries = await service.list_entries(
|
||||
journal_id=journal.id,
|
||||
filters=ListEntriesFilter(
|
||||
@@ -660,7 +710,7 @@ async def list_agent_entries(
|
||||
task_id=params.task_id,
|
||||
limit=params.limit,
|
||||
offset=params.offset,
|
||||
include_private=False, # Can't see other agents' private entries
|
||||
include_private=True, # Full access for authorized readers
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ async def send_notification(
|
||||
|
||||
Enforces permission rules:
|
||||
- Only PMs, Board members, and Auditor can send notifications
|
||||
- Cell PMs can only notify members of their own cell
|
||||
- Cell PMs can notify their cell members, Main PM, or other Cell PMs
|
||||
- Main PM, Auditor, and CEO can notify anyone
|
||||
"""
|
||||
# Look up the sending agent to get their agent_id string
|
||||
@@ -121,21 +121,21 @@ async def send_notification(
|
||||
detail="Agent not found",
|
||||
)
|
||||
|
||||
# Look up recipient agent_ids
|
||||
recipient_ids = []
|
||||
# Look up recipient slugs for permission checking
|
||||
recipient_slugs = []
|
||||
for recipient_uuid in data.to_agents:
|
||||
recipient_result = await db.execute(
|
||||
select(AgentTable).where(AgentTable.id == recipient_uuid)
|
||||
)
|
||||
recipient = recipient_result.scalar_one_or_none()
|
||||
if recipient:
|
||||
recipient_ids.append(str(recipient.id))
|
||||
recipient_slugs.append(recipient.slug)
|
||||
|
||||
# Validate notification permissions using enforcement layer
|
||||
try:
|
||||
validate_notification_permission(
|
||||
sender_id=agent.slug,
|
||||
recipients=recipient_ids,
|
||||
recipients=recipient_slugs,
|
||||
)
|
||||
except NotificationPermissionError as e:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -378,6 +378,7 @@ async def create_session_for_tasks(
|
||||
req = SessionForTasksCreate(
|
||||
task_ids=data.task_ids,
|
||||
channel_slug=data.channel_slug,
|
||||
scope=data.scope,
|
||||
relationship_type=rel_type,
|
||||
)
|
||||
|
||||
@@ -388,11 +389,22 @@ async def create_session_for_tasks(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
) from e
|
||||
except ConflictError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(e),
|
||||
) from e
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e),
|
||||
) from e
|
||||
except Exception as e:
|
||||
# Catch-all for debugging - expose actual error
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Session creation failed: {type(e).__name__}: {e}",
|
||||
) from e
|
||||
|
||||
session_response = SessionResponse(
|
||||
id=require_uuid(session.id),
|
||||
|
||||
+15
-12
@@ -14,6 +14,7 @@ from roboco.api.deps import (
|
||||
CurrentAgentContext,
|
||||
DbSession,
|
||||
PermissionServiceDep,
|
||||
get_permission_service,
|
||||
)
|
||||
from roboco.api.schemas.sessions import (
|
||||
SessionTaskLinkResponse,
|
||||
@@ -23,7 +24,6 @@ from roboco.api.schemas.tasks import (
|
||||
CheckpointRequest,
|
||||
ClaimRequest,
|
||||
CommitRequest,
|
||||
ListTasksQuery,
|
||||
ProgressRequest,
|
||||
QANotes,
|
||||
SoftBlockRequest,
|
||||
@@ -92,6 +92,7 @@ async def create_task(
|
||||
parent_task_id=data.parent_task_id,
|
||||
target_date=data.target_date,
|
||||
estimated_complexity=data.estimated_complexity,
|
||||
status=data.status,
|
||||
)
|
||||
task = await service.create(req)
|
||||
await db.commit()
|
||||
@@ -102,8 +103,9 @@ async def create_task(
|
||||
async def list_tasks(
|
||||
db: DbSession,
|
||||
agent: CurrentAgentContext,
|
||||
permissions: PermissionServiceDep,
|
||||
params: Annotated[ListTasksQuery, Query()],
|
||||
team: Team | None = None,
|
||||
status: TaskStatus | None = None,
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
) -> list[TaskResponse]:
|
||||
"""
|
||||
List tasks with optional filters.
|
||||
@@ -114,10 +116,11 @@ async def list_tasks(
|
||||
- Cell members: Can only see own cell's tasks
|
||||
"""
|
||||
service = get_task_service(db)
|
||||
permissions = get_permission_service()
|
||||
|
||||
# Determine effective team filter based on permissions
|
||||
can_view_all = permissions.can_perform_task_action(agent, TaskAction.VIEW_ALL)
|
||||
effective_team = params.team
|
||||
effective_team = team
|
||||
|
||||
if not can_view_all:
|
||||
# Cell members can only see their own team's tasks
|
||||
@@ -127,14 +130,14 @@ async def list_tasks(
|
||||
# No team assigned - return empty list
|
||||
return []
|
||||
|
||||
if effective_team and params.status:
|
||||
tasks = await service.list_by_team(effective_team, params.status, params.limit)
|
||||
if effective_team and status:
|
||||
tasks = await service.list_by_team(effective_team, status, limit)
|
||||
elif effective_team:
|
||||
tasks = await service.list_by_team(effective_team, limit=params.limit)
|
||||
elif params.status:
|
||||
tasks = await service.list_by_status(params.status)
|
||||
tasks = await service.list_by_team(effective_team, limit=limit)
|
||||
elif status:
|
||||
tasks = await service.list_by_status(status)
|
||||
else:
|
||||
tasks = await service.list_all(params.limit, params.offset)
|
||||
tasks = await service.list_all(limit)
|
||||
|
||||
return task_list_to_response(tasks)
|
||||
|
||||
@@ -1114,8 +1117,8 @@ async def activate_task(
|
||||
|
||||
REQUIRES: Task must have at least one linked session.
|
||||
"""
|
||||
# Check PM permission
|
||||
if not permissions.can_perform_task_action(agent, "create_tasks"):
|
||||
# Check PM permission (CREATE permission required for activation)
|
||||
if not permissions.can_perform_task_action(agent, TaskAction.CREATE):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only PMs and management can activate tasks",
|
||||
|
||||
+1
-1
@@ -507,7 +507,7 @@ class SessionTaskTable(Base):
|
||||
"ix_session_tasks_primary_per_task",
|
||||
"task_id",
|
||||
unique=True,
|
||||
postgresql_where=(is_primary == True), # noqa: E712
|
||||
postgresql_where=(is_primary.is_(True)),
|
||||
),
|
||||
# Fast lookups
|
||||
Index("ix_session_tasks_task_id", "task_id"),
|
||||
|
||||
@@ -17,6 +17,12 @@ from roboco.enforcement.channel_access import (
|
||||
get_agent_channels,
|
||||
validate_channel_access,
|
||||
)
|
||||
from roboco.enforcement.journal_perms import (
|
||||
JournalAccessDeniedError,
|
||||
can_read_journal,
|
||||
get_readable_journals,
|
||||
validate_journal_access,
|
||||
)
|
||||
from roboco.enforcement.notification_perms import (
|
||||
NotificationPermissionError,
|
||||
get_notification_scope,
|
||||
@@ -46,19 +52,23 @@ __all__ = [
|
||||
"ROLE_RESTRICTED_TRANSITIONS",
|
||||
"VALID_TRANSITIONS",
|
||||
"ChannelAccessDeniedError",
|
||||
"JournalAccessDeniedError",
|
||||
"NotificationPermissionError",
|
||||
"TaskClaimContext",
|
||||
"TaskLifecycleError",
|
||||
"TaskOwnershipError",
|
||||
"can_agent_transition",
|
||||
"can_read_journal",
|
||||
"can_review_task",
|
||||
"get_agent_channels",
|
||||
"get_notification_scope",
|
||||
"get_readable_journals",
|
||||
"get_valid_transitions",
|
||||
"is_active_state",
|
||||
"is_terminal_state",
|
||||
"is_waiting_state",
|
||||
"validate_channel_access",
|
||||
"validate_journal_access",
|
||||
"validate_notification_permission",
|
||||
"validate_task_claim",
|
||||
"validate_task_ownership",
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"""
|
||||
Journal Permission Enforcement
|
||||
|
||||
Validates who can read whose journal entries.
|
||||
Permission model mirrors notification/channel access:
|
||||
- Cell members can read each other's journals (full access including private)
|
||||
- Cell PMs can read other cells' journals
|
||||
- Main PM can read all cell journals
|
||||
- Board can read all journals except CEO/Auditor
|
||||
- Auditor has silent read access to all journals
|
||||
- CEO can read all journals
|
||||
"""
|
||||
|
||||
from roboco.agents_config import (
|
||||
get_agent_cell,
|
||||
get_agent_role,
|
||||
)
|
||||
from roboco.exceptions import RobocoError
|
||||
|
||||
|
||||
class JournalAccessDeniedError(RobocoError):
|
||||
"""Raised when an agent doesn't have permission to read a journal."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reader_id: str,
|
||||
owner_id: str,
|
||||
message: str | None = None,
|
||||
):
|
||||
self.reader_id = reader_id
|
||||
self.owner_id = owner_id
|
||||
super().__init__(
|
||||
code="JOURNAL_ACCESS_DENIED",
|
||||
message=message or f"Agent {reader_id} cannot read journal of {owner_id}",
|
||||
details={
|
||||
"reader_id": reader_id,
|
||||
"owner_id": owner_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Protected journals - only readable by CEO/Auditor themselves
|
||||
PROTECTED_JOURNALS = frozenset(["ceo", "auditor"])
|
||||
|
||||
|
||||
def _is_same_cell(agent1: str, agent2: str) -> bool:
|
||||
"""Check if two agents are in the same cell."""
|
||||
cell1 = get_agent_cell(agent1)
|
||||
cell2 = get_agent_cell(agent2)
|
||||
return cell1 is not None and cell1 == cell2
|
||||
|
||||
|
||||
def can_read_journal(reader_id: str, owner_id: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Check if reader can access owner's journal.
|
||||
|
||||
Returns:
|
||||
Tuple of (can_read, reason)
|
||||
"""
|
||||
# Self-access always allowed
|
||||
if reader_id == owner_id:
|
||||
return True, "OK"
|
||||
|
||||
reader_role = get_agent_role(reader_id)
|
||||
owner_role = get_agent_role(owner_id)
|
||||
|
||||
# CEO and Auditor journals are protected
|
||||
if owner_id in PROTECTED_JOURNALS or owner_role in ("ceo", "auditor"):
|
||||
# Only CEO can read Auditor's journal and vice versa
|
||||
if reader_role == "ceo":
|
||||
return True, "OK"
|
||||
if reader_role == "auditor":
|
||||
return True, "OK"
|
||||
return False, f"Cannot read {owner_role}'s journal - protected"
|
||||
|
||||
# CEO can read all journals (except protected, handled above)
|
||||
if reader_role == "ceo":
|
||||
return True, "OK"
|
||||
|
||||
# Auditor has silent read access to all journals
|
||||
if reader_role == "auditor":
|
||||
return True, "OK"
|
||||
|
||||
# Board members can read all cell journals
|
||||
if reader_role in ("product_owner", "head_marketing"):
|
||||
return True, "OK"
|
||||
|
||||
# Main PM can read all cell journals
|
||||
if reader_role == "main_pm":
|
||||
return True, "OK"
|
||||
|
||||
# Cell PM can read:
|
||||
# 1. Own cell members
|
||||
# 2. Other Cell PMs
|
||||
# 3. Main PM (for coordination)
|
||||
if reader_role == "cell_pm":
|
||||
# Own cell members
|
||||
if _is_same_cell(reader_id, owner_id):
|
||||
return True, "OK"
|
||||
# Other Cell PMs
|
||||
if owner_role == "cell_pm":
|
||||
return True, "OK"
|
||||
# Main PM
|
||||
if owner_role == "main_pm":
|
||||
return True, "OK"
|
||||
return False, "Cell PM can only read journals of cell members, other PMs"
|
||||
|
||||
# Cell members (developer, qa, documenter) can read same cell journals
|
||||
if reader_role in ("developer", "qa", "documenter"):
|
||||
if _is_same_cell(reader_id, owner_id):
|
||||
return True, "OK"
|
||||
return False, "You can only read journals of your cell members"
|
||||
|
||||
return False, "Unknown role - access denied"
|
||||
|
||||
|
||||
def validate_journal_access(reader_id: str, owner_id: str) -> bool:
|
||||
"""
|
||||
Validate reader can access owner's journal.
|
||||
|
||||
Args:
|
||||
reader_id: The agent trying to read (slug)
|
||||
owner_id: The journal owner (slug)
|
||||
|
||||
Returns:
|
||||
True if allowed
|
||||
|
||||
Raises:
|
||||
JournalAccessDeniedError: If access denied
|
||||
"""
|
||||
can_read, reason = can_read_journal(reader_id, owner_id)
|
||||
if not can_read:
|
||||
raise JournalAccessDeniedError(
|
||||
reader_id=reader_id,
|
||||
owner_id=owner_id,
|
||||
message=reason,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def get_readable_journals(reader_id: str) -> dict:
|
||||
"""
|
||||
Get information about what journals an agent can read.
|
||||
|
||||
Returns:
|
||||
Dict with scope information
|
||||
"""
|
||||
role = get_agent_role(reader_id)
|
||||
cell = get_agent_cell(reader_id)
|
||||
|
||||
if role in ("ceo", "auditor"):
|
||||
return {"scope": "all", "description": "Can read all journals"}
|
||||
|
||||
if role in ("product_owner", "head_marketing", "main_pm"):
|
||||
return {
|
||||
"scope": "all_cells",
|
||||
"description": "Can read all cell journals",
|
||||
"excludes": ["ceo", "auditor"],
|
||||
}
|
||||
|
||||
if role == "cell_pm":
|
||||
return {
|
||||
"scope": "cell_plus_pms",
|
||||
"cell": cell,
|
||||
"description": f"Can read {cell} cell journals and other PM journals",
|
||||
}
|
||||
|
||||
if role in ("developer", "qa", "documenter"):
|
||||
return {
|
||||
"scope": "cell",
|
||||
"cell": cell,
|
||||
"description": f"Can read {cell} cell journals only",
|
||||
}
|
||||
|
||||
return {"scope": "none", "description": "Unknown role"}
|
||||
@@ -44,29 +44,39 @@ def _can_send_to_recipient(sender_id: str, recipient_id: str) -> tuple[bool, str
|
||||
role = get_agent_role(sender_id)
|
||||
permissions = NOTIFICATION_PERMISSIONS.get(role, {"can_send": False})
|
||||
|
||||
can_send = False
|
||||
reason = ""
|
||||
|
||||
if not permissions.get("can_send", False):
|
||||
return False, f"Agents with role '{role}' cannot send notifications"
|
||||
reason = f"Agents with role '{role}' cannot send notifications"
|
||||
else:
|
||||
scope = permissions.get("scope", [])
|
||||
|
||||
scope = permissions.get("scope", [])
|
||||
if scope == "all":
|
||||
can_send = True
|
||||
reason = "OK"
|
||||
elif scope == "cell":
|
||||
sender_cell = get_agent_cell(sender_id)
|
||||
recipient_cell = get_agent_cell(recipient_id)
|
||||
recipient_role = get_agent_role(recipient_id)
|
||||
|
||||
if scope == "all":
|
||||
return True, "OK"
|
||||
if (sender_cell and sender_cell == recipient_cell) or recipient_role in {
|
||||
"main_pm",
|
||||
"cell_pm",
|
||||
}:
|
||||
can_send = True
|
||||
reason = "OK"
|
||||
else:
|
||||
reason = (
|
||||
"Cell PM can only notify cell members, Main PM, or other Cell PMs"
|
||||
)
|
||||
elif isinstance(scope, list) and recipient_id in scope:
|
||||
can_send = True
|
||||
reason = "OK"
|
||||
else:
|
||||
reason = f"Cannot send notifications to {recipient_id}"
|
||||
|
||||
if scope == "cell":
|
||||
sender_cell = get_agent_cell(sender_id)
|
||||
recipient_cell = get_agent_cell(recipient_id)
|
||||
|
||||
if sender_cell and sender_cell == recipient_cell:
|
||||
return True, "OK"
|
||||
return (
|
||||
False,
|
||||
f"Cell PM can only notify members of their own cell ({sender_cell})",
|
||||
)
|
||||
|
||||
if isinstance(scope, list) and recipient_id in scope:
|
||||
return True, "OK"
|
||||
|
||||
return False, f"Cannot send notifications to {recipient_id}"
|
||||
return can_send, reason
|
||||
|
||||
|
||||
def validate_notification_permission(
|
||||
|
||||
@@ -18,6 +18,8 @@ from typing import Any
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from roboco.agents_config import get_agent_cell, get_agent_role
|
||||
from roboco.enforcement.journal_perms import can_read_journal, get_readable_journals
|
||||
from roboco.llm import ToonAdapter
|
||||
from roboco.mcp.schemas import (
|
||||
DecisionLogInput,
|
||||
@@ -57,6 +59,7 @@ async def _handle_journal_entry(
|
||||
"title": data.title,
|
||||
"content": data.content,
|
||||
"task_id": data.task_id,
|
||||
"session_id": data.session_id,
|
||||
"tags": data.tags,
|
||||
"is_private": data.is_private,
|
||||
}
|
||||
@@ -86,6 +89,7 @@ async def _handle_reflect(
|
||||
"""Handle task reflection creation."""
|
||||
payload = {
|
||||
"task_id": data.task_id,
|
||||
"session_id": data.session_id,
|
||||
"title": data.title,
|
||||
"what_done": data.what_done,
|
||||
"what_learned": data.what_learned,
|
||||
@@ -283,6 +287,50 @@ async def _handle_recent(
|
||||
return {"entries": entries, "count": len(entries) if entries else 0}
|
||||
|
||||
|
||||
async def _handle_team_entries(
|
||||
target_agent: str,
|
||||
reader_agent: str,
|
||||
params: dict[str, Any],
|
||||
client: ApiClient,
|
||||
) -> dict[str, Any]:
|
||||
"""Handle reading another agent's journal entries."""
|
||||
# Check permission
|
||||
can_read, reason = can_read_journal(reader_agent, target_agent)
|
||||
if not can_read:
|
||||
return format_error_response("ACCESS_DENIED", reason)
|
||||
|
||||
entries, error = await client.get_or_error(
|
||||
f"/journals/{target_agent}/entries",
|
||||
params=params,
|
||||
error_code="READ_FAILED",
|
||||
error_message=f"Failed to read {target_agent}'s journal",
|
||||
)
|
||||
if error:
|
||||
return error
|
||||
|
||||
return {
|
||||
"agent": target_agent,
|
||||
"entries": entries,
|
||||
"count": len(entries) if entries else 0,
|
||||
"guidance": f"Showing entries from {target_agent}'s journal.",
|
||||
}
|
||||
|
||||
|
||||
def _get_journal_scope(agent_id: str) -> dict[str, Any]:
|
||||
"""Get information about what journals an agent can read."""
|
||||
scope_info = get_readable_journals(agent_id)
|
||||
role = get_agent_role(agent_id)
|
||||
cell = get_agent_cell(agent_id)
|
||||
|
||||
return {
|
||||
"your_agent": agent_id,
|
||||
"your_role": role,
|
||||
"your_cell": cell,
|
||||
"scope": scope_info,
|
||||
"guidance": scope_info.get("description", ""),
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MCP SERVER FACTORY
|
||||
# =============================================================================
|
||||
@@ -384,6 +432,47 @@ def create_journal_mcp_server(agent_id: str) -> FastMCP:
|
||||
"""
|
||||
return await _handle_recent(entry_type, task_id, limit, client)
|
||||
|
||||
# =========================================================================
|
||||
# TEAM JOURNAL ACCESS TOOLS
|
||||
# =========================================================================
|
||||
|
||||
@mcp.tool()
|
||||
async def roboco_journal_read_team(
|
||||
target_agent: str,
|
||||
entry_type: str | None = None,
|
||||
task_id: str | None = None,
|
||||
limit: int = 10,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Read journal entries from a teammate.
|
||||
|
||||
Cell members can read each other's journals (including private entries).
|
||||
PMs can read across cells. Main PM, Board, and Auditor have broader access.
|
||||
|
||||
Args:
|
||||
target_agent: Agent slug to read from (e.g., "be-dev-1", "be-qa")
|
||||
entry_type: Optional filter by type
|
||||
task_id: Optional filter by task
|
||||
limit: Max entries to return (default 10)
|
||||
"""
|
||||
max_limit = 50
|
||||
params: dict[str, Any] = {"limit": min(limit, max_limit)}
|
||||
if entry_type:
|
||||
params["entry_type"] = entry_type
|
||||
if task_id:
|
||||
params["task_id"] = task_id
|
||||
|
||||
return await _handle_team_entries(target_agent, agent_id, params, client)
|
||||
|
||||
@mcp.tool()
|
||||
def roboco_journal_scope() -> dict[str, Any]:
|
||||
"""
|
||||
Get information about which journals you can access.
|
||||
|
||||
Shows your role, cell, and what other agents' journals you can read.
|
||||
"""
|
||||
return _get_journal_scope(agent_id)
|
||||
|
||||
return mcp
|
||||
|
||||
|
||||
|
||||
+28
-15
@@ -48,25 +48,38 @@ def _can_send_notification(sender_id: str, recipient_id: str) -> tuple[bool, str
|
||||
role = get_agent_role(sender_id)
|
||||
permissions = NOTIFICATION_PERMISSIONS.get(role, {"can_send": False})
|
||||
|
||||
can_send = False
|
||||
reason = ""
|
||||
|
||||
if not permissions.get("can_send", False):
|
||||
return False, f"Agents with role '{role}' cannot send notifications"
|
||||
reason = f"Agents with role '{role}' cannot send notifications"
|
||||
else:
|
||||
scope = permissions.get("scope", [])
|
||||
|
||||
scope = permissions.get("scope", [])
|
||||
if scope == "all":
|
||||
can_send = True
|
||||
reason = "OK"
|
||||
elif scope == "cell":
|
||||
has_cell, sender_cell = _check_cell_scope(sender_id)
|
||||
recipient_cell = get_agent_cell(recipient_id)
|
||||
recipient_role = get_agent_role(recipient_id)
|
||||
|
||||
if scope == "all":
|
||||
return True, "OK"
|
||||
# Cell PM can notify their own cell members
|
||||
if (has_cell and sender_cell == recipient_cell) or recipient_role in {
|
||||
"main_pm",
|
||||
"cell_pm",
|
||||
}:
|
||||
can_send = True
|
||||
reason = "OK"
|
||||
else:
|
||||
reason = "Cell PM can only notify cell members, Main PM, or other PMs"
|
||||
elif isinstance(scope, list) and recipient_id in scope:
|
||||
can_send = True
|
||||
reason = "OK"
|
||||
else:
|
||||
reason = f"You cannot send notifications to {recipient_id}"
|
||||
|
||||
if scope == "cell":
|
||||
has_cell, sender_cell = _check_cell_scope(sender_id)
|
||||
recipient_cell = get_agent_cell(recipient_id)
|
||||
if has_cell and sender_cell == recipient_cell:
|
||||
return True, "OK"
|
||||
return False, f"Cell PM can only notify own cell members ({sender_cell})"
|
||||
|
||||
if isinstance(scope, list) and recipient_id in scope:
|
||||
return True, "OK"
|
||||
|
||||
return False, f"You cannot send notifications to {recipient_id}"
|
||||
return can_send, reason
|
||||
|
||||
|
||||
# Valid notification types and priorities
|
||||
|
||||
@@ -21,6 +21,7 @@ class JournalEntryInput(BaseModel):
|
||||
description="Type: general, task_reflection, decision_log, learning, struggle",
|
||||
)
|
||||
task_id: str | None = Field(default=None, description="Optional related task")
|
||||
session_id: str | None = Field(default=None, description="Optional related session")
|
||||
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||
is_private: bool = Field(
|
||||
default=False, description="If true, only you and CEO/Auditor can see"
|
||||
@@ -31,6 +32,7 @@ class TaskReflectionInput(BaseModel):
|
||||
"""Input for creating a task reflection entry."""
|
||||
|
||||
task_id: str = Field(..., description="The task UUID you're reflecting on")
|
||||
session_id: str | None = Field(default=None, description="Optional session context")
|
||||
title: str = Field(..., description="Reflection title")
|
||||
what_done: str = Field(..., description="What was accomplished")
|
||||
what_learned: str = Field(..., description="Key learnings from this task")
|
||||
@@ -177,6 +179,10 @@ class TaskCreateInput(BaseModel):
|
||||
complexity: str = Field(
|
||||
default="medium", description="Complexity: low, medium, high, critical"
|
||||
)
|
||||
status: str = Field(
|
||||
default="backlog",
|
||||
description="Status: 'backlog' (default) or 'pending' (ready for work)",
|
||||
)
|
||||
|
||||
|
||||
class TaskAssignInput(BaseModel):
|
||||
|
||||
+73
-53
@@ -72,20 +72,8 @@ from roboco.mcp.tasks.handlers import (
|
||||
from roboco.mcp.utils import ApiClient
|
||||
|
||||
|
||||
def create_task_mcp_server(agent_id: str) -> FastMCP: # noqa: PLR0915
|
||||
"""
|
||||
Create a Task MCP server for a specific agent.
|
||||
|
||||
The agent_id is embedded in the server to enforce ownership rules.
|
||||
|
||||
Args:
|
||||
agent_id: The agent identifier (e.g., "be-dev-1")
|
||||
|
||||
Returns:
|
||||
Configured FastMCP server
|
||||
"""
|
||||
mcp = FastMCP(f"roboco-task-{agent_id}", json_response=True)
|
||||
client = ApiClient(agent_id)
|
||||
def _register_core_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||
"""Register core task lifecycle tools."""
|
||||
|
||||
@mcp.tool()
|
||||
async def roboco_task_scan(team: str | None = None) -> dict[str, Any]:
|
||||
@@ -215,6 +203,24 @@ def create_task_mcp_server(agent_id: str) -> FastMCP: # noqa: PLR0915
|
||||
client, task_id, message, percentage, agent_id
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
async def roboco_agent_idle() -> dict[str, Any]:
|
||||
"""
|
||||
Signal that you have no work and should go idle.
|
||||
|
||||
Call this when roboco_task_scan returns no tasks.
|
||||
Your container will be terminated to save resources.
|
||||
You will be automatically respawned when new work is available.
|
||||
|
||||
Returns:
|
||||
Confirmation of idle state
|
||||
"""
|
||||
return await handle_agent_idle(client, agent_id)
|
||||
|
||||
|
||||
def _register_blocking_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||
"""Register blocking/unblocking/pause tools."""
|
||||
|
||||
@mcp.tool()
|
||||
async def roboco_task_block(
|
||||
task_id: str, reason: str, blocker_type: str, what_needed: str
|
||||
@@ -288,6 +294,10 @@ def create_task_mcp_server(agent_id: str) -> FastMCP: # noqa: PLR0915
|
||||
)
|
||||
return await handle_task_pause(client, data, agent_id)
|
||||
|
||||
|
||||
def _register_qa_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||
"""Register QA and verification tools."""
|
||||
|
||||
@mcp.tool()
|
||||
async def roboco_task_submit_verification(task_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -412,23 +422,9 @@ def create_task_mcp_server(agent_id: str) -> FastMCP: # noqa: PLR0915
|
||||
"""
|
||||
return await handle_task_complete(client, task_id, agent_id)
|
||||
|
||||
@mcp.tool()
|
||||
async def roboco_agent_idle() -> dict[str, Any]:
|
||||
"""
|
||||
Signal that you have no work and should go idle.
|
||||
|
||||
Call this when roboco_task_scan returns no tasks.
|
||||
Your container will be terminated to save resources.
|
||||
You will be automatically respawned when new work is available.
|
||||
|
||||
Returns:
|
||||
Confirmation of idle state
|
||||
"""
|
||||
return await handle_agent_idle(client, agent_id)
|
||||
|
||||
# =========================================================================
|
||||
# PM DELEGATION TOOLS
|
||||
# =========================================================================
|
||||
def _register_pm_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||
"""Register PM delegation and management tools."""
|
||||
|
||||
@mcp.tool()
|
||||
async def roboco_task_create(data: TaskCreateInput) -> dict[str, Any]:
|
||||
@@ -446,7 +442,8 @@ def create_task_mcp_server(agent_id: str) -> FastMCP: # noqa: PLR0915
|
||||
|
||||
Args:
|
||||
data: TaskCreateInput with title, description, acceptance_criteria,
|
||||
team, and optional parent_task_id, assigned_to, priority
|
||||
team, and optional parent_task_id, assigned_to, priority, status.
|
||||
Use status="backlog" for subtasks needing session setup.
|
||||
|
||||
Returns:
|
||||
Created task with next step guidance
|
||||
@@ -533,9 +530,34 @@ def create_task_mcp_server(agent_id: str) -> FastMCP: # noqa: PLR0915
|
||||
)
|
||||
return await handle_task_escalate(client, input_data, agent_id)
|
||||
|
||||
# =========================================================================
|
||||
# PM SESSION TOOLS
|
||||
# =========================================================================
|
||||
@mcp.tool()
|
||||
async def roboco_task_activate(task_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
Activate a task from BACKLOG to PENDING status (PM only).
|
||||
|
||||
This is the FINAL STEP in task setup. After creating and assigning
|
||||
a task, you MUST:
|
||||
1. Create a session: roboco_session_create_for_tasks()
|
||||
2. Activate the task: roboco_task_activate()
|
||||
|
||||
Only after activation will the orchestrator spawn agents to work on it.
|
||||
|
||||
ENFORCEMENT:
|
||||
- Only PMs and management can activate tasks
|
||||
- Task must be in BACKLOG status
|
||||
- Task MUST have at least one linked session
|
||||
|
||||
Args:
|
||||
task_id: The task UUID to activate
|
||||
|
||||
Returns:
|
||||
Activated task with PENDING status
|
||||
"""
|
||||
return await handle_task_activate(client, task_id, agent_id)
|
||||
|
||||
|
||||
def _register_session_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||
"""Register session management tools."""
|
||||
|
||||
@mcp.tool()
|
||||
async def roboco_session_create_for_tasks(
|
||||
@@ -634,30 +656,28 @@ def create_task_mcp_server(agent_id: str) -> FastMCP: # noqa: PLR0915
|
||||
"""
|
||||
return await handle_session_get_for_task(client, task_id, agent_id)
|
||||
|
||||
@mcp.tool()
|
||||
async def roboco_task_activate(task_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
Activate a task from BACKLOG to PENDING status (PM only).
|
||||
|
||||
This is the FINAL STEP in task setup. After creating and assigning
|
||||
a task, you MUST:
|
||||
1. Create a session: roboco_session_create_for_tasks()
|
||||
2. Activate the task: roboco_task_activate()
|
||||
def create_task_mcp_server(agent_id: str) -> FastMCP:
|
||||
"""
|
||||
Create a Task MCP server for a specific agent.
|
||||
|
||||
Only after activation will the orchestrator spawn agents to work on it.
|
||||
The agent_id is embedded in the server to enforce ownership rules.
|
||||
|
||||
ENFORCEMENT:
|
||||
- Only PMs and management can activate tasks
|
||||
- Task must be in BACKLOG status
|
||||
- Task MUST have at least one linked session
|
||||
Args:
|
||||
agent_id: The agent identifier (e.g., "be-dev-1")
|
||||
|
||||
Args:
|
||||
task_id: The task UUID to activate
|
||||
Returns:
|
||||
Configured FastMCP server
|
||||
"""
|
||||
mcp = FastMCP(f"roboco-task-{agent_id}", json_response=True)
|
||||
client = ApiClient(agent_id)
|
||||
|
||||
Returns:
|
||||
Activated task with PENDING status
|
||||
"""
|
||||
return await handle_task_activate(client, task_id, agent_id)
|
||||
# Register all tools via helper functions
|
||||
_register_core_tools(mcp, client, agent_id)
|
||||
_register_blocking_tools(mcp, client, agent_id)
|
||||
_register_qa_tools(mcp, client, agent_id)
|
||||
_register_pm_tools(mcp, client, agent_id)
|
||||
_register_session_tools(mcp, client, agent_id)
|
||||
|
||||
return mcp
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any
|
||||
|
||||
from fastapi import status
|
||||
|
||||
from roboco.agents_config import get_agent_cell, get_agent_role
|
||||
from roboco.mcp.schemas import TaskBlockInput, TaskPauseInput
|
||||
from roboco.mcp.tasks import format_task_response
|
||||
from roboco.mcp.tasks.handlers._helpers import validate_task_ownership
|
||||
@@ -50,16 +51,34 @@ async def handle_task_block(
|
||||
|
||||
return format_task_response(
|
||||
block_resp.json(),
|
||||
"WAIT_OR_SWITCH",
|
||||
"RESOLVE_BLOCKER",
|
||||
f"Task blocked: {data.reason}\n\n"
|
||||
"Options:\n"
|
||||
"1. WAIT - If resolution expected soon, poll for updates\n"
|
||||
"2. SWITCH - Call roboco_task_scan to work on another task\n"
|
||||
"3. ESCALATE - Message your PM if this is urgent\n\n"
|
||||
"The blocker has been communicated. You'll be notified when resolved.",
|
||||
"1. UNBLOCK - When resolved, call roboco_task_unblock() to resume\n"
|
||||
"2. WAIT - If waiting for external resolution\n"
|
||||
"3. SWITCH - Call roboco_task_scan for other work\n"
|
||||
"4. ESCALATE - Message your PM if urgent\n\n"
|
||||
"Blocker recorded. You'll be notified when resolved.",
|
||||
)
|
||||
|
||||
|
||||
def _can_unblock_task(agent_id: str, task: dict) -> tuple[bool, str]:
|
||||
"""Check if agent can unblock a task. PMs can unblock any task in their cell."""
|
||||
role = get_agent_role(agent_id)
|
||||
agent_cell = get_agent_cell(agent_id)
|
||||
task_team = task.get("team")
|
||||
|
||||
# Main PM, Board, CEO can unblock anything
|
||||
if role in ("main_pm", "product_owner", "head_marketing", "auditor", "ceo"):
|
||||
return True, "OK"
|
||||
|
||||
# Cell PM can unblock any task in their cell
|
||||
if role == "cell_pm" and agent_cell and agent_cell == task_team:
|
||||
return True, "OK"
|
||||
|
||||
return False, "Only PMs can unblock tasks"
|
||||
|
||||
|
||||
async def handle_task_unblock(
|
||||
client: ApiClient, task_id: str, agent_id: str
|
||||
) -> dict[str, Any]:
|
||||
@@ -70,12 +89,17 @@ async def handle_task_unblock(
|
||||
|
||||
task = task_resp.json()
|
||||
|
||||
if error := await validate_task_ownership(task, agent_id, client):
|
||||
return error
|
||||
|
||||
if task.get("status") != "blocked":
|
||||
return format_error_response("INVALID_STATE", "Task is not blocked")
|
||||
|
||||
# Check PM permissions or task ownership
|
||||
can_unblock, _ = _can_unblock_task(agent_id, task)
|
||||
if not can_unblock and await validate_task_ownership(task, agent_id, client):
|
||||
return format_error_response(
|
||||
"NOT_AUTHORIZED",
|
||||
"You cannot unblock this task. Must be assigned or a PM.",
|
||||
)
|
||||
|
||||
unblock_resp = await client.post(f"/tasks/{task_id}/unblock")
|
||||
|
||||
if not unblock_resp.ok:
|
||||
|
||||
@@ -154,6 +154,7 @@ def _build_task_payload(input_data: TaskCreateInput) -> dict[str, Any]:
|
||||
"team": input_data.team,
|
||||
"priority": input_data.priority,
|
||||
"estimated_complexity": input_data.complexity,
|
||||
"status": input_data.status, # Always included, defaults to "backlog"
|
||||
}
|
||||
if input_data.parent_task_id:
|
||||
payload["parent_task_id"] = input_data.parent_task_id
|
||||
@@ -162,10 +163,17 @@ def _build_task_payload(input_data: TaskCreateInput) -> dict[str, Any]:
|
||||
|
||||
def _format_create_guidance(task: dict[str, Any], assigned_to: str | None) -> str:
|
||||
"""Format guidance message for task creation."""
|
||||
guidance = f"Task created successfully. ID: {task['id']}. "
|
||||
if assigned_to:
|
||||
task_status = task.get("status", "pending")
|
||||
guidance = f"Task created successfully. ID: {task['id']}. Status: {task_status}. "
|
||||
|
||||
if task_status == "backlog":
|
||||
guidance += (
|
||||
f"Assigned to: {assigned_to} (pending). "
|
||||
"Task is in BACKLOG. Create session with "
|
||||
"roboco_session_create_for_tasks, then roboco_task_activate."
|
||||
)
|
||||
elif assigned_to:
|
||||
guidance += (
|
||||
f"Assigned to: {assigned_to}. "
|
||||
"Orchestrator will spawn them to claim and work on it."
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -62,11 +62,12 @@ def get_next_step_guidance(status: str) -> tuple[str, str]:
|
||||
"If blocked, call roboco_task_block with details.",
|
||||
),
|
||||
"blocked": (
|
||||
"WAIT_OR_SWITCH",
|
||||
"You are blocked. Options: "
|
||||
"1) Wait for resolution (if expected soon), "
|
||||
"2) Switch to another task (call roboco_task_scan), "
|
||||
"3) Escalate to PM if urgent.",
|
||||
"RESOLVE_BLOCKER",
|
||||
"Task is blocked. Options: "
|
||||
"1) UNBLOCK - If resolved, call roboco_task_unblock() to resume. "
|
||||
"2) WAIT - If waiting for external resolution. "
|
||||
"3) SWITCH - Call roboco_task_scan for other work. "
|
||||
"4) ESCALATE - If urgent, message your PM.",
|
||||
),
|
||||
"paused": (
|
||||
"RESUME_OR_SCAN",
|
||||
|
||||
@@ -72,6 +72,7 @@ class AgentContext:
|
||||
agent_id: UUID
|
||||
role: AgentRole
|
||||
team: Team | None = None
|
||||
slug: str | None = None # Agent slug (e.g., "be-dev-1")
|
||||
|
||||
@property
|
||||
def level(self) -> PermissionLevel:
|
||||
|
||||
@@ -234,6 +234,7 @@ class TaskCreate(RobocoBase):
|
||||
parent_task_id: UUID | None = None
|
||||
target_date: datetime | None = None
|
||||
estimated_complexity: Complexity = Complexity.MEDIUM
|
||||
status: TaskStatus | None = None # PM can set 'backlog' for subtasks needing setup
|
||||
|
||||
|
||||
class TaskUpdate(RobocoBase):
|
||||
@@ -270,3 +271,4 @@ class TaskCreateRequest:
|
||||
parent_task_id: UUID | None = None
|
||||
target_date: datetime | None = None
|
||||
estimated_complexity: Complexity = field(default=Complexity.MEDIUM)
|
||||
status: TaskStatus | None = None # PM can set BACKLOG for subtasks
|
||||
|
||||
@@ -76,6 +76,20 @@ class JournalService(BaseService):
|
||||
|
||||
return await resolve_agent_uuid(self.session, agent_id_or_slug)
|
||||
|
||||
async def get_agent_slug(self, agent_id: UUID) -> str | None:
|
||||
"""
|
||||
Get an agent's slug from their UUID.
|
||||
|
||||
Args:
|
||||
agent_id: The agent's UUID
|
||||
|
||||
Returns:
|
||||
The agent's slug (e.g., "be-dev-1"), or None if not found
|
||||
"""
|
||||
from roboco.services.repositories import get_agent_slug
|
||||
|
||||
return await get_agent_slug(self.session, agent_id)
|
||||
|
||||
# =========================================================================
|
||||
# JOURNAL CRUD
|
||||
# =========================================================================
|
||||
|
||||
@@ -452,11 +452,14 @@ class MessagingService(BaseService):
|
||||
if existing_primary.scalar_one_or_none():
|
||||
raise ConflictError(f"Task {task_id} already has a primary session")
|
||||
|
||||
# Handle both enum and string (RobocoBase uses use_enum_values=True)
|
||||
rel_value = getattr(relationship_type, "value", relationship_type)
|
||||
|
||||
link = SessionTaskTable(
|
||||
session_id=session_id,
|
||||
task_id=task_id,
|
||||
is_primary=is_primary,
|
||||
relationship_type=relationship_type.value,
|
||||
relationship_type=rel_value,
|
||||
added_by=added_by,
|
||||
)
|
||||
|
||||
@@ -468,7 +471,7 @@ class MessagingService(BaseService):
|
||||
session_id=str(session_id),
|
||||
task_id=str(task_id),
|
||||
is_primary=is_primary,
|
||||
relationship_type=relationship_type.value,
|
||||
relationship_type=rel_value,
|
||||
)
|
||||
return link
|
||||
|
||||
@@ -534,9 +537,8 @@ class MessagingService(BaseService):
|
||||
)
|
||||
|
||||
if relationship_type:
|
||||
query = query.where(
|
||||
SessionTaskTable.relationship_type == relationship_type.value
|
||||
)
|
||||
rel_value = getattr(relationship_type, "value", relationship_type)
|
||||
query = query.where(SessionTaskTable.relationship_type == rel_value)
|
||||
|
||||
query = query.order_by(SessionTaskTable.added_at.desc())
|
||||
result = await self.session.execute(query)
|
||||
|
||||
@@ -8,7 +8,9 @@ for database operations. Reduces boilerplate in services.
|
||||
from roboco.services.repositories.base import BaseRepository
|
||||
from roboco.services.repositories.query_helpers import (
|
||||
agent_id_filter,
|
||||
get_agent_slug,
|
||||
pagination,
|
||||
resolve_agent_identity,
|
||||
resolve_agent_uuid,
|
||||
status_filter,
|
||||
team_filter,
|
||||
@@ -18,7 +20,9 @@ from roboco.services.repositories.query_helpers import (
|
||||
__all__ = [
|
||||
"BaseRepository",
|
||||
"agent_id_filter",
|
||||
"get_agent_slug",
|
||||
"pagination",
|
||||
"resolve_agent_identity",
|
||||
"resolve_agent_uuid",
|
||||
"status_filter",
|
||||
"team_filter",
|
||||
|
||||
@@ -188,6 +188,66 @@ async def resolve_agent_uuid(
|
||||
return UUID(str(agent_uuid))
|
||||
|
||||
|
||||
async def resolve_agent_identity(
|
||||
db: AsyncSession,
|
||||
agent_id_or_slug: str,
|
||||
) -> tuple[UUID, str] | None:
|
||||
"""
|
||||
Resolve an agent identity to both UUID and slug.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
agent_id_or_slug: Either a UUID string or agent slug (e.g., "be-dev-1")
|
||||
|
||||
Returns:
|
||||
Tuple of (UUID, slug), or None if not found
|
||||
"""
|
||||
# First, try to parse as UUID
|
||||
try:
|
||||
agent_uuid = UUID(agent_id_or_slug)
|
||||
# It's a UUID, look up the slug
|
||||
result = await db.execute(
|
||||
select(AgentTable.slug).where(AgentTable.id == agent_uuid)
|
||||
)
|
||||
slug = result.scalar_one_or_none()
|
||||
if slug is None:
|
||||
return None
|
||||
return (agent_uuid, slug)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Not a UUID, try to look up by slug
|
||||
result = await db.execute(
|
||||
select(AgentTable.id).where(AgentTable.slug == agent_id_or_slug)
|
||||
)
|
||||
agent_uuid = result.scalar_one_or_none()
|
||||
|
||||
if agent_uuid is None:
|
||||
return None
|
||||
|
||||
return (UUID(str(agent_uuid)), agent_id_or_slug)
|
||||
|
||||
|
||||
async def get_agent_slug(
|
||||
db: AsyncSession,
|
||||
agent_id: UUID,
|
||||
) -> str | None:
|
||||
"""
|
||||
Get an agent's slug from their UUID.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
agent_id: The agent's UUID
|
||||
|
||||
Returns:
|
||||
The agent's slug (e.g., "be-dev-1"), or None if not found
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(AgentTable.slug).where(AgentTable.id == agent_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_agent_by_slug(
|
||||
db: AsyncSession,
|
||||
slug: str,
|
||||
|
||||
@@ -118,11 +118,8 @@ class TaskService(BaseService):
|
||||
"""
|
||||
Create a new task.
|
||||
|
||||
Tasks are created with BACKLOG status by default. PM must:
|
||||
1. Create a session for the task
|
||||
2. Call activate() to transition to PENDING
|
||||
|
||||
This ensures every task has a session before work begins.
|
||||
Default status is PENDING. PM can pass status=BACKLOG when creating
|
||||
subtasks that need session setup before activation.
|
||||
"""
|
||||
task = TaskTable(
|
||||
title=req.title,
|
||||
@@ -134,7 +131,7 @@ class TaskService(BaseService):
|
||||
parent_task_id=req.parent_task_id,
|
||||
target_date=req.target_date,
|
||||
estimated_complexity=req.estimated_complexity,
|
||||
status=TaskStatus.BACKLOG,
|
||||
status=req.status if req.status else TaskStatus.PENDING,
|
||||
)
|
||||
self.session.add(task)
|
||||
await self.session.flush()
|
||||
|
||||
Reference in New Issue
Block a user