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_submit_qa(task_id, dev_notes, handoff_summary)` - Submit for QA review
|
||||||
- `roboco_task_escalate(task_id, reason)` - Escalate issues to PM
|
- `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_entry(data)` - General journal entry
|
||||||
- `roboco_journal_reflect(data)` - Task reflection (what done, learned, struggled)
|
- `roboco_journal_reflect(data)` - Task reflection (what done, learned, struggled)
|
||||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
- `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_search(query, top_k)` - Search past journal entries
|
||||||
- `roboco_journal_recent(limit)` - Get recent 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:**
|
**Communication:**
|
||||||
- `roboco_channel_list()` - List available channels
|
- `roboco_channel_list()` - List available channels
|
||||||
- `roboco_channel_history(channel_slug, limit?)` - Read channel history
|
- `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:**
|
**IMPORTANT: Two types of notes with different audiences:**
|
||||||
|
|
||||||
1. **Task Notes (for QA)** - Via `roboco_task_submit_qa` - QA and Documenter WILL see these
|
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)`
|
**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
|
```json
|
||||||
{
|
{
|
||||||
"task_id": "{task_id}",
|
"task_id": "{task_id}",
|
||||||
@@ -375,12 +379,15 @@ tools:
|
|||||||
- roboco_task_submit_verification, roboco_task_submit_qa
|
- roboco_task_submit_verification, roboco_task_submit_qa
|
||||||
- roboco_task_escalate, roboco_agent_idle
|
- roboco_task_escalate, roboco_agent_idle
|
||||||
|
|
||||||
# Journal
|
# Journal (Your Own)
|
||||||
- roboco_journal_entry, roboco_journal_reflect
|
- roboco_journal_entry, roboco_journal_reflect
|
||||||
- roboco_journal_decision, roboco_journal_learning
|
- roboco_journal_decision, roboco_journal_learning
|
||||||
- roboco_journal_struggle, roboco_journal_search
|
- roboco_journal_struggle, roboco_journal_search
|
||||||
- roboco_journal_recent
|
- roboco_journal_recent
|
||||||
|
|
||||||
|
# Team Journals (Read Cell Members)
|
||||||
|
- roboco_journal_read_team, roboco_journal_scope
|
||||||
|
|
||||||
# Communication
|
# Communication
|
||||||
- roboco_channel_list, roboco_channel_history
|
- roboco_channel_list, roboco_channel_history
|
||||||
- roboco_message_send, roboco_ask_question
|
- roboco_message_send, roboco_ask_question
|
||||||
@@ -416,4 +423,7 @@ permissions:
|
|||||||
- update_own_tasks
|
- update_own_tasks
|
||||||
- escalate_tasks
|
- escalate_tasks
|
||||||
- request_qa_review
|
- 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_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||||
- `roboco_task_create(data)` - Create subtasks for developers
|
- `roboco_task_create(data)` - Create subtasks for developers
|
||||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||||
|
- `roboco_task_unblock(task_id)` - Unblock a blocked task (PM only)
|
||||||
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
||||||
|
|
||||||
**Session Management (Work Sessions for Tasks):**
|
**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_unlink_task(session_id, task_id)` - Remove task from session
|
||||||
- `roboco_session_get_for_task(task_id)` - Get sessions linked to a task
|
- `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_entry(data)` - General journal entry
|
||||||
- `roboco_journal_reflect(data)` - Task reflection
|
- `roboco_journal_reflect(data)` - Task reflection
|
||||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
- `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_struggle(data)` - Document a challenge
|
||||||
- `roboco_journal_search(query, top_k)` - Search past entries
|
- `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:**
|
**Communication:**
|
||||||
- `roboco_channel_list()` - List available channels
|
- `roboco_channel_list()` - List available channels
|
||||||
- `roboco_channel_history(channel_slug)` - Read channel history
|
- `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`
|
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
||||||
- Do NOT keep tasks for yourself - delegate to developers!
|
- 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)`
|
**Tool:** `roboco_session_create_for_tasks(data)`
|
||||||
|
|
||||||
After delegating, you MUST create a work session for the task:
|
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
|
- Subtasks auto-inherit parent task's primary session
|
||||||
- Full audit trail preserved
|
- Full audit trail preserved
|
||||||
|
|
||||||
### 7.6. ACTIVATE TASK (REQUIRED)
|
### 7b. ACTIVATE TASK (REQUIRED)
|
||||||
**Tool:** `roboco_task_activate(task_id)`
|
**Tool:** `roboco_task_activate(task_id)`
|
||||||
|
|
||||||
After creating the session, activate the task to make it ready for work:
|
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")
|
roboco_task_activate("task-uuid")
|
||||||
```
|
```
|
||||||
|
|
||||||
**IMPORTANT:** Tasks are created with BACKLOG status. They will NOT be
|
**IMPORTANT:** When creating subtasks, pass `status: "backlog"` to prevent
|
||||||
picked up by the orchestrator until you activate them. This ensures
|
orchestrator from picking them up before you set up sessions. Activate
|
||||||
every task has a session before work begins.
|
when ready.
|
||||||
|
|
||||||
**Task flow:**
|
**Task flow:**
|
||||||
```
|
```
|
||||||
CREATE (backlog) → SESSION → ACTIVATE (pending) → Orchestrator spawns dev
|
CREATE (status: backlog) → SESSION → ACTIVATE (pending) → Orchestrator spawns dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 8. COMMUNICATE
|
### 8. COMMUNICATE
|
||||||
@@ -261,10 +266,13 @@ the task for your final review.
|
|||||||
|
|
||||||
### Developer is Blocked
|
### Developer is Blocked
|
||||||
1. Check the blocker: `roboco_task_get(task_id)`
|
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()`
|
3. Cross-cell issue? → Escalate: `roboco_escalate()`
|
||||||
4. Reassign dev to different task if wait is long
|
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
|
### Task Needs Clarification
|
||||||
1. Document what's unclear
|
1. Document what's unclear
|
||||||
2. Ask in #backend-cell or escalate to Main PM
|
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_scan, roboco_task_get, roboco_task_claim
|
||||||
- roboco_task_start, roboco_task_plan, roboco_task_progress
|
- roboco_task_start, roboco_task_plan, roboco_task_progress
|
||||||
- roboco_task_create, roboco_task_assign, roboco_task_activate
|
- roboco_task_create, roboco_task_assign, roboco_task_activate
|
||||||
- roboco_task_complete
|
- roboco_task_unblock, roboco_task_complete
|
||||||
|
|
||||||
# Session Management (REQUIRED before activation)
|
# Session Management (REQUIRED before activation)
|
||||||
- roboco_session_create_for_tasks, roboco_session_link_task
|
- roboco_session_create_for_tasks, roboco_session_link_task
|
||||||
- roboco_session_unlink_task, roboco_session_get_for_task
|
- roboco_session_unlink_task, roboco_session_get_for_task
|
||||||
|
|
||||||
# Journal
|
# Journal (Your Own)
|
||||||
- roboco_journal_entry, roboco_journal_decision
|
- roboco_journal_entry, roboco_journal_decision
|
||||||
- roboco_journal_learning, roboco_journal_struggle
|
- roboco_journal_learning, roboco_journal_struggle
|
||||||
|
|
||||||
|
# Team Journals (Read Cell Members + Other PMs)
|
||||||
|
- roboco_journal_read_team, roboco_journal_scope
|
||||||
|
|
||||||
# Communication
|
# Communication
|
||||||
- roboco_message_send, roboco_channel_history
|
- roboco_message_send, roboco_channel_history
|
||||||
|
|
||||||
@@ -413,8 +424,14 @@ permissions:
|
|||||||
- assign_tasks
|
- assign_tasks
|
||||||
- change_priority
|
- change_priority
|
||||||
- close_tasks
|
- close_tasks
|
||||||
|
- unblock_tasks
|
||||||
- view_all_cell_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:
|
notify_targets:
|
||||||
- be-dev-1
|
- be-dev-1
|
||||||
- be-dev-2
|
- be-dev-2
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ You interact with RoboCo systems through MCP tools:
|
|||||||
- `roboco_journal_struggle(data)` - Document a challenge
|
- `roboco_journal_struggle(data)` - Document a challenge
|
||||||
- `roboco_journal_search(query, top_k)` - Search past journal entries
|
- `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:**
|
**Communication:**
|
||||||
- `roboco_channel_list()` - List available channels
|
- `roboco_channel_list()` - List available channels
|
||||||
- `roboco_channel_history(channel_slug, limit?)` - Read channel history
|
- `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
|
- `handoff_summary` - Summary for reviewers
|
||||||
- `progress_updates` - Timestamped progress with percentages
|
- `progress_updates` - Timestamped progress with percentages
|
||||||
- Commits list
|
- 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:**
|
**Verifying journal-related acceptance criteria:**
|
||||||
- Developer's personal journal (journals are private per agent)
|
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:**
|
This returns journal entries filtered by task. Look for the required entry type/content.
|
||||||
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
|
|
||||||
|
|
||||||
Read all available notes. If dev_notes is empty or unclear, that's a QA FAIL reason.
|
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_qa_pass, roboco_task_qa_fail
|
||||||
- roboco_task_escalate, roboco_agent_idle
|
- roboco_task_escalate, roboco_agent_idle
|
||||||
|
|
||||||
# Journal
|
# Journal (Your Own)
|
||||||
- roboco_journal_entry, roboco_journal_reflect
|
- roboco_journal_entry, roboco_journal_reflect
|
||||||
- roboco_journal_decision, roboco_journal_learning
|
- roboco_journal_decision, roboco_journal_learning
|
||||||
- roboco_journal_struggle, roboco_journal_search
|
- roboco_journal_struggle, roboco_journal_search
|
||||||
|
|
||||||
|
# Team Journals (Read Cell Members)
|
||||||
|
- roboco_journal_read_team, roboco_journal_scope
|
||||||
|
|
||||||
# Communication
|
# Communication
|
||||||
- roboco_channel_list, roboco_channel_history
|
- roboco_channel_list, roboco_channel_history
|
||||||
- roboco_message_send, roboco_ask_question
|
- roboco_message_send, roboco_ask_question
|
||||||
@@ -299,6 +307,9 @@ permissions:
|
|||||||
- qa-all
|
- qa-all
|
||||||
- all-hands
|
- all-hands
|
||||||
|
|
||||||
|
journals_read:
|
||||||
|
- backend cell members (be-dev-1, be-dev-2, be-doc, be-pm)
|
||||||
|
|
||||||
task_permissions:
|
task_permissions:
|
||||||
- claim_qa_tasks
|
- claim_qa_tasks
|
||||||
- qa_pass_tasks
|
- qa_pass_tasks
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ You interact with RoboCo systems through MCP tools:
|
|||||||
**Task Management:**
|
**Task Management:**
|
||||||
- `roboco_task_scan()` - Check for tasks requiring your attention
|
- `roboco_task_scan()` - Check for tasks requiring your attention
|
||||||
- `roboco_task_get(task_id)` - Get task details
|
- `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)
|
- `roboco_task_activate(task_id)` - Activate task from BACKLOG to PENDING (after session created)
|
||||||
|
|
||||||
**Session Management (Cross-Cell Work Sessions):**
|
**Session Management (Cross-Cell Work Sessions):**
|
||||||
@@ -128,22 +128,22 @@ Translate Board direction into cell priorities:
|
|||||||
- Balance workload across cells
|
- Balance workload across cells
|
||||||
|
|
||||||
### DISTRIBUTE
|
### DISTRIBUTE
|
||||||
Push work to cells. Tasks are created with BACKLOG status - they won't be
|
Push work to cells. Use BACKLOG status when you need time to set up sessions
|
||||||
visible to orchestrators until you activate them.
|
before work begins.
|
||||||
|
|
||||||
**Standard Distribution Workflow:**
|
**Standard Distribution Workflow:**
|
||||||
|
|
||||||
**1. CREATE TASKS (BACKLOG)**
|
**1. CREATE TASKS (with BACKLOG for setup)**
|
||||||
Create high-level task records for each cell:
|
Create task records for each cell with explicit BACKLOG status:
|
||||||
```python
|
```python
|
||||||
roboco_task_create({
|
roboco_task_create({
|
||||||
"title": "Build preferences API",
|
"title": "Build preferences API",
|
||||||
"description": "GET/PUT /api/v1/users/{id}/preferences",
|
"description": "GET/PUT /api/v1/users/{id}/preferences",
|
||||||
"team": "backend",
|
"team": "backend",
|
||||||
"acceptance_criteria": ["Endpoint implemented", "Tests passing"],
|
"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)**
|
**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_submit_qa(task_id, dev_notes, handoff_summary)` - Submit for QA review
|
||||||
- `roboco_task_escalate(task_id, reason)` - Escalate issues to PM
|
- `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_entry(data)` - General journal entry
|
||||||
- `roboco_journal_reflect(data)` - Task reflection (what done, learned, struggled)
|
- `roboco_journal_reflect(data)` - Task reflection (what done, learned, struggled)
|
||||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
- `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_search(query, top_k)` - Search past journal entries
|
||||||
- `roboco_journal_recent(limit)` - Get recent 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:**
|
**Communication:**
|
||||||
- `roboco_channel_list()` - List available channels
|
- `roboco_channel_list()` - List available channels
|
||||||
- `roboco_channel_history(channel_slug, limit?)` - Read channel history
|
- `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:**
|
**IMPORTANT: Two types of notes with different audiences:**
|
||||||
|
|
||||||
1. **Task Notes (for QA)** - Via `roboco_task_submit_qa` - QA and Documenter WILL see these
|
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)`
|
**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.
|
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_submit_verification, roboco_task_submit_qa
|
||||||
- roboco_task_escalate, roboco_agent_idle
|
- roboco_task_escalate, roboco_agent_idle
|
||||||
|
|
||||||
# Journal
|
# Journal (Your Own)
|
||||||
- roboco_journal_entry, roboco_journal_reflect
|
- roboco_journal_entry, roboco_journal_reflect
|
||||||
- roboco_journal_decision, roboco_journal_learning
|
- roboco_journal_decision, roboco_journal_learning
|
||||||
- roboco_journal_struggle, roboco_journal_search
|
- roboco_journal_struggle, roboco_journal_search
|
||||||
- roboco_journal_recent
|
- roboco_journal_recent
|
||||||
|
|
||||||
|
# Team Journals (Read Cell Members)
|
||||||
|
- roboco_journal_read_team, roboco_journal_scope
|
||||||
|
|
||||||
# Communication
|
# Communication
|
||||||
- roboco_channel_list, roboco_channel_history
|
- roboco_channel_list, roboco_channel_history
|
||||||
- roboco_message_send, roboco_ask_question
|
- roboco_message_send, roboco_ask_question
|
||||||
@@ -326,4 +333,7 @@ permissions:
|
|||||||
- update_own_tasks
|
- update_own_tasks
|
||||||
- escalate_tasks
|
- escalate_tasks
|
||||||
- request_qa_review
|
- 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_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||||
- `roboco_task_create(data)` - Create subtasks for developers
|
- `roboco_task_create(data)` - Create subtasks for developers
|
||||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||||
|
- `roboco_task_unblock(task_id)` - Unblock a blocked task (PM only)
|
||||||
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
||||||
|
|
||||||
**Session Management (Work Sessions for Tasks):**
|
**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_unlink_task(session_id, task_id)` - Remove task from session
|
||||||
- `roboco_session_get_for_task(task_id)` - Get sessions linked to a task
|
- `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_entry(data)` - General journal entry
|
||||||
- `roboco_journal_reflect(data)` - Task reflection
|
- `roboco_journal_reflect(data)` - Task reflection
|
||||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
- `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_struggle(data)` - Document a challenge
|
||||||
- `roboco_journal_search(query, top_k)` - Search past entries
|
- `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:**
|
**Communication:**
|
||||||
- `roboco_channel_list()` - List available channels
|
- `roboco_channel_list()` - List available channels
|
||||||
- `roboco_channel_history(channel_slug)` - Read channel history
|
- `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`
|
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
||||||
- Do NOT keep tasks for yourself - delegate to developers!
|
- 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)`
|
**Tool:** `roboco_session_create_for_tasks(data)`
|
||||||
|
|
||||||
After delegating, you MUST create a work session for the task:
|
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
|
- QA and documenter see full context when reviewing
|
||||||
- Subtasks auto-inherit parent task's primary session
|
- Subtasks auto-inherit parent task's primary session
|
||||||
|
|
||||||
### 7.6. ACTIVATE TASK (REQUIRED)
|
### 7b. ACTIVATE TASK (REQUIRED)
|
||||||
**Tool:** `roboco_task_activate(task_id)`
|
**Tool:** `roboco_task_activate(task_id)`
|
||||||
|
|
||||||
After creating the session, activate the task:
|
After creating the session, activate the task:
|
||||||
@@ -186,9 +191,9 @@ After creating the session, activate the task:
|
|||||||
roboco_task_activate("task-uuid")
|
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
|
### 8. COMMUNICATE
|
||||||
@@ -277,13 +282,16 @@ Can these be added?
|
|||||||
1. Confirm exact API need (endpoint, schema)
|
1. Confirm exact API need (endpoint, schema)
|
||||||
2. Contact BE-PM with specific ask
|
2. Contact BE-PM with specific ask
|
||||||
3. If long wait: have dev use mock data
|
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
|
### Developer is Blocked on Design
|
||||||
1. Confirm what's missing (states, specs, assets)
|
1. Confirm what's missing (states, specs, assets)
|
||||||
2. Contact UX-PM with specific ask
|
2. Contact UX-PM with specific ask
|
||||||
3. If minor: can dev proceed with best judgment?
|
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
|
### All Subtasks Complete
|
||||||
1. Review parent task: `roboco_task_get(parent_id)`
|
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_scan, roboco_task_get, roboco_task_claim
|
||||||
- roboco_task_start, roboco_task_plan, roboco_task_progress
|
- roboco_task_start, roboco_task_plan, roboco_task_progress
|
||||||
- roboco_task_create, roboco_task_assign, roboco_task_activate
|
- roboco_task_create, roboco_task_assign, roboco_task_activate
|
||||||
- roboco_task_complete
|
- roboco_task_unblock, roboco_task_complete
|
||||||
|
|
||||||
# Session Management (REQUIRED before activation)
|
# Session Management (REQUIRED before activation)
|
||||||
- roboco_session_create_for_tasks, roboco_session_link_task
|
- roboco_session_create_for_tasks, roboco_session_link_task
|
||||||
- roboco_session_unlink_task, roboco_session_get_for_task
|
- roboco_session_unlink_task, roboco_session_get_for_task
|
||||||
|
|
||||||
# Journal
|
# Journal (Your Own)
|
||||||
- roboco_journal_entry, roboco_journal_decision
|
- roboco_journal_entry, roboco_journal_decision
|
||||||
- roboco_journal_learning, roboco_journal_struggle
|
- roboco_journal_learning, roboco_journal_struggle
|
||||||
|
|
||||||
|
# Team Journals (Read Cell Members + Other PMs)
|
||||||
|
- roboco_journal_read_team, roboco_journal_scope
|
||||||
|
|
||||||
# Communication
|
# Communication
|
||||||
- roboco_message_send, roboco_channel_history
|
- roboco_message_send, roboco_channel_history
|
||||||
|
|
||||||
@@ -414,8 +425,14 @@ permissions:
|
|||||||
- assign_tasks
|
- assign_tasks
|
||||||
- change_priority
|
- change_priority
|
||||||
- close_tasks
|
- close_tasks
|
||||||
|
- unblock_tasks
|
||||||
- view_all_cell_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:
|
notify_targets:
|
||||||
- fe-dev-1
|
- fe-dev-1
|
||||||
- fe-dev-2
|
- 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_qa_fail(task_id, qa_notes, issues)` - Reject with issues
|
||||||
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
||||||
|
|
||||||
**Journal:**
|
**Journal (Your Own):**
|
||||||
- `roboco_journal_entry(data)` - General journal entry
|
- `roboco_journal_entry(data)` - General journal entry
|
||||||
- `roboco_journal_reflect(data)` - Task reflection
|
- `roboco_journal_reflect(data)` - Task reflection
|
||||||
- `roboco_journal_decision(data)` - Log decisions
|
- `roboco_journal_decision(data)` - Log decisions
|
||||||
- `roboco_journal_learning(data)` - Document learnings
|
- `roboco_journal_learning(data)` - Document learnings
|
||||||
- `roboco_journal_struggle(data)` - Document challenges
|
- `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:**
|
**Communication:**
|
||||||
- `roboco_channel_list()` - List channels
|
- `roboco_channel_list()` - List channels
|
||||||
- `roboco_channel_history(channel_slug)` - Read history
|
- `roboco_channel_history(channel_slug)` - Read history
|
||||||
@@ -78,15 +82,13 @@ If none: `roboco_agent_idle()`
|
|||||||
- `dev_notes` - Developer's work evidence
|
- `dev_notes` - Developer's work evidence
|
||||||
- `progress_updates` - Timestamped progress with percentages
|
- `progress_updates` - Timestamped progress with percentages
|
||||||
- Design specs and acceptance criteria
|
- 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:**
|
**Verifying journal-related acceptance criteria:**
|
||||||
- Developer's personal journal (private)
|
If criteria mentions journaling (e.g., "journal contains a report"), verify directly:
|
||||||
|
```python
|
||||||
**Journal-related acceptance criteria:**
|
roboco_journal_read_team("fe-dev-1", task_id="{task_id}", limit=10)
|
||||||
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
|
|
||||||
|
|
||||||
If dev_notes is empty, that's a valid FAIL reason.
|
If dev_notes is empty, that's a valid FAIL reason.
|
||||||
|
|
||||||
@@ -156,12 +158,17 @@ capabilities:
|
|||||||
- journaling
|
- journaling
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
|
# Task Management
|
||||||
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
||||||
- roboco_task_start, roboco_task_progress
|
- roboco_task_start, roboco_task_progress
|
||||||
- roboco_task_qa_pass, roboco_task_qa_fail
|
- roboco_task_qa_pass, roboco_task_qa_fail
|
||||||
- roboco_task_escalate, roboco_agent_idle
|
- roboco_task_escalate, roboco_agent_idle
|
||||||
|
# Journal (Your Own)
|
||||||
- roboco_journal_entry, roboco_journal_reflect
|
- roboco_journal_entry, roboco_journal_reflect
|
||||||
- roboco_journal_decision, roboco_journal_learning
|
- 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_channel_list, roboco_channel_history
|
||||||
- roboco_message_send, roboco_ask_question
|
- roboco_message_send, roboco_ask_question
|
||||||
```
|
```
|
||||||
@@ -183,6 +190,9 @@ permissions:
|
|||||||
- qa-all
|
- qa-all
|
||||||
- all-hands
|
- all-hands
|
||||||
|
|
||||||
|
journals_read:
|
||||||
|
- frontend cell members (fe-dev-1, fe-dev-2, fe-doc, fe-pm)
|
||||||
|
|
||||||
task_permissions:
|
task_permissions:
|
||||||
- claim_qa_tasks
|
- claim_qa_tasks
|
||||||
- qa_pass_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_submit_qa(task_id, dev_notes, handoff_summary)` - Submit for QA review
|
||||||
- `roboco_task_escalate(task_id, reason)` - Escalate issues to PM
|
- `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_entry(data)` - General journal entry
|
||||||
- `roboco_journal_reflect(data)` - Task reflection (what done, learned, struggled)
|
- `roboco_journal_reflect(data)` - Task reflection (what done, learned, struggled)
|
||||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
- `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_search(query, top_k)` - Search past journal entries
|
||||||
- `roboco_journal_recent(limit)` - Get recent 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:**
|
**Communication:**
|
||||||
- `roboco_channel_list()` - List available channels
|
- `roboco_channel_list()` - List available channels
|
||||||
- `roboco_channel_history(channel_slug, limit?)` - Read channel history
|
- `roboco_channel_history(channel_slug, limit?)` - Read channel history
|
||||||
@@ -155,7 +159,7 @@ Checklist:
|
|||||||
**IMPORTANT: Two types of notes with different audiences:**
|
**IMPORTANT: Two types of notes with different audiences:**
|
||||||
|
|
||||||
1. **Task Notes (for QA)** - Via `roboco_task_submit_qa` - QA and Documenter WILL see these
|
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)`
|
**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.
|
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_submit_verification, roboco_task_submit_qa
|
||||||
- roboco_task_escalate, roboco_agent_idle
|
- roboco_task_escalate, roboco_agent_idle
|
||||||
|
|
||||||
# Journal
|
# Journal (Your Own)
|
||||||
- roboco_journal_entry, roboco_journal_reflect
|
- roboco_journal_entry, roboco_journal_reflect
|
||||||
- roboco_journal_decision, roboco_journal_learning
|
- roboco_journal_decision, roboco_journal_learning
|
||||||
- roboco_journal_struggle, roboco_journal_search
|
- roboco_journal_struggle, roboco_journal_search
|
||||||
- roboco_journal_recent
|
- roboco_journal_recent
|
||||||
|
|
||||||
|
# Team Journals (Read Cell Members)
|
||||||
|
- roboco_journal_read_team, roboco_journal_scope
|
||||||
|
|
||||||
# Communication
|
# Communication
|
||||||
- roboco_channel_list, roboco_channel_history
|
- roboco_channel_list, roboco_channel_history
|
||||||
- roboco_message_send, roboco_ask_question
|
- roboco_message_send, roboco_ask_question
|
||||||
@@ -293,4 +300,7 @@ permissions:
|
|||||||
- update_own_tasks
|
- update_own_tasks
|
||||||
- escalate_tasks
|
- escalate_tasks
|
||||||
- request_qa_review
|
- 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_progress(task_id, message, percentage)` - Add progress notes (percentage 0-100 required)
|
||||||
- `roboco_task_create(data)` - Create subtasks for designers
|
- `roboco_task_create(data)` - Create subtasks for designers
|
||||||
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
- `roboco_task_assign(task_id, agent_slug)` - Assign task to an agent
|
||||||
|
- `roboco_task_unblock(task_id)` - Unblock a blocked task (PM only)
|
||||||
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
- `roboco_task_complete(task_id)` - Complete a parent task after subtasks done
|
||||||
|
|
||||||
**Session Management (Work Sessions for Tasks):**
|
**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_unlink_task(session_id, task_id)` - Remove task from session
|
||||||
- `roboco_session_get_for_task(task_id)` - Get sessions linked to a task
|
- `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_entry(data)` - General journal entry
|
||||||
- `roboco_journal_reflect(data)` - Task reflection
|
- `roboco_journal_reflect(data)` - Task reflection
|
||||||
- `roboco_journal_decision(data)` - Log a decision with options/rationale
|
- `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_struggle(data)` - Document a challenge
|
||||||
- `roboco_journal_search(query, top_k)` - Search past entries
|
- `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:**
|
**Communication:**
|
||||||
- `roboco_channel_list()` - List available channels
|
- `roboco_channel_list()` - List available channels
|
||||||
- `roboco_channel_history(channel_slug)` - Read channel history
|
- `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`
|
- Every subtask MUST have both `parent_task_id` AND `assigned_to`
|
||||||
- Do NOT keep tasks for yourself - delegate to designers!
|
- 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)`
|
**Tool:** `roboco_session_create_for_tasks(data)`
|
||||||
|
|
||||||
After delegating, you MUST create a work session for the task:
|
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
|
- QA and documenter see design context
|
||||||
- Frontend can review design discussion history
|
- Frontend can review design discussion history
|
||||||
|
|
||||||
### 7.6. ACTIVATE TASK (REQUIRED)
|
### 7b. ACTIVATE TASK (REQUIRED)
|
||||||
**Tool:** `roboco_task_activate(task_id)`
|
**Tool:** `roboco_task_activate(task_id)`
|
||||||
|
|
||||||
After creating the session, activate the task:
|
After creating the session, activate the task:
|
||||||
@@ -188,7 +193,7 @@ roboco_task_activate("task-uuid")
|
|||||||
|
|
||||||
**Task flow:**
|
**Task flow:**
|
||||||
```
|
```
|
||||||
CREATE (backlog) → SESSION → ACTIVATE (pending) → Orchestrator spawns dev
|
CREATE (status: backlog) → SESSION → ACTIVATE (pending) → Orchestrator spawns dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 8. COMMUNICATE
|
### 8. COMMUNICATE
|
||||||
@@ -279,6 +284,15 @@ UX-PM: @ProductOwner Question on TASK-055:
|
|||||||
3. Assess UX-Dev workload - can they pivot?
|
3. Assess UX-Dev workload - can they pivot?
|
||||||
4. Communicate realistic timeline to FE-PM
|
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
|
### Design Requirements Unclear
|
||||||
1. Document specific questions
|
1. Document specific questions
|
||||||
2. Escalate to Product Owner with specific asks
|
2. Escalate to Product Owner with specific asks
|
||||||
@@ -367,16 +381,19 @@ tools:
|
|||||||
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
||||||
- roboco_task_start, roboco_task_plan, roboco_task_progress
|
- roboco_task_start, roboco_task_plan, roboco_task_progress
|
||||||
- roboco_task_create, roboco_task_assign, roboco_task_activate
|
- roboco_task_create, roboco_task_assign, roboco_task_activate
|
||||||
- roboco_task_complete
|
- roboco_task_unblock, roboco_task_complete
|
||||||
|
|
||||||
# Session Management (REQUIRED before activation)
|
# Session Management (REQUIRED before activation)
|
||||||
- roboco_session_create_for_tasks, roboco_session_link_task
|
- roboco_session_create_for_tasks, roboco_session_link_task
|
||||||
- roboco_session_unlink_task, roboco_session_get_for_task
|
- roboco_session_unlink_task, roboco_session_get_for_task
|
||||||
|
|
||||||
# Journal
|
# Journal (Your Own)
|
||||||
- roboco_journal_entry, roboco_journal_decision
|
- roboco_journal_entry, roboco_journal_decision
|
||||||
- roboco_journal_learning, roboco_journal_struggle
|
- roboco_journal_learning, roboco_journal_struggle
|
||||||
|
|
||||||
|
# Team Journals (Read Cell Members + Other PMs)
|
||||||
|
- roboco_journal_read_team, roboco_journal_scope
|
||||||
|
|
||||||
# Communication
|
# Communication
|
||||||
- roboco_message_send, roboco_channel_history
|
- roboco_message_send, roboco_channel_history
|
||||||
|
|
||||||
@@ -414,8 +431,14 @@ permissions:
|
|||||||
- assign_tasks
|
- assign_tasks
|
||||||
- change_priority
|
- change_priority
|
||||||
- close_tasks
|
- close_tasks
|
||||||
|
- unblock_tasks
|
||||||
- view_all_cell_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:
|
notify_targets:
|
||||||
- ux-dev
|
- ux-dev
|
||||||
- ux-qa
|
- 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_qa_fail(task_id, qa_notes, issues)` - Reject with issues
|
||||||
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
- `roboco_task_escalate(task_id, reason)` - Escalate to PM
|
||||||
|
|
||||||
**Journal:**
|
**Journal (Your Own):**
|
||||||
- `roboco_journal_entry(data)` - General journal entry
|
- `roboco_journal_entry(data)` - General journal entry
|
||||||
- `roboco_journal_reflect(data)` - Task reflection
|
- `roboco_journal_reflect(data)` - Task reflection
|
||||||
- `roboco_journal_decision(data)` - Log decisions
|
- `roboco_journal_decision(data)` - Log decisions
|
||||||
- `roboco_journal_learning(data)` - Document learnings
|
- `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:**
|
**Communication:**
|
||||||
- `roboco_channel_list()` - List channels
|
- `roboco_channel_list()` - List channels
|
||||||
- `roboco_channel_history(channel_slug)` - Read history
|
- `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
|
- `dev_notes` - Designer's work evidence and Figma links
|
||||||
- `progress_updates` - Timestamped progress with percentages
|
- `progress_updates` - Timestamped progress with percentages
|
||||||
- Requirements and acceptance criteria
|
- Requirements and acceptance criteria
|
||||||
|
- **Cell member journals** - You can read journals of UX-Dev, UX-Documenter
|
||||||
|
|
||||||
**What you CANNOT see:**
|
**Verifying journal-related acceptance criteria:**
|
||||||
- Designer's personal journal (private)
|
If criteria mentions journaling (e.g., "journal contains design rationale"), verify directly:
|
||||||
|
```python
|
||||||
**Journal-related acceptance criteria:**
|
roboco_journal_read_team("ux-dev", task_id="{task_id}", limit=10)
|
||||||
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
|
|
||||||
|
|
||||||
If dev_notes is empty or no Figma link provided, that's a valid FAIL reason.
|
If dev_notes is empty or no Figma link provided, that's a valid FAIL reason.
|
||||||
|
|
||||||
@@ -154,12 +156,17 @@ capabilities:
|
|||||||
- journaling
|
- journaling
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
|
# Task Management
|
||||||
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
- roboco_task_scan, roboco_task_get, roboco_task_claim
|
||||||
- roboco_task_start, roboco_task_progress
|
- roboco_task_start, roboco_task_progress
|
||||||
- roboco_task_qa_pass, roboco_task_qa_fail
|
- roboco_task_qa_pass, roboco_task_qa_fail
|
||||||
- roboco_task_escalate, roboco_agent_idle
|
- roboco_task_escalate, roboco_agent_idle
|
||||||
|
# Journal (Your Own)
|
||||||
- roboco_journal_entry, roboco_journal_reflect
|
- roboco_journal_entry, roboco_journal_reflect
|
||||||
- roboco_journal_decision, roboco_journal_learning
|
- 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_channel_list, roboco_channel_history
|
||||||
- roboco_message_send, roboco_ask_question
|
- roboco_message_send, roboco_ask_question
|
||||||
```
|
```
|
||||||
@@ -181,6 +188,9 @@ permissions:
|
|||||||
- qa-all
|
- qa-all
|
||||||
- all-hands
|
- all-hands
|
||||||
|
|
||||||
|
journals_read:
|
||||||
|
- uxui cell members (ux-dev, ux-doc, ux-pm)
|
||||||
|
|
||||||
task_permissions:
|
task_permissions:
|
||||||
- claim_qa_tasks
|
- claim_qa_tasks
|
||||||
- qa_pass_tasks
|
- qa_pass_tasks
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ def upgrade() -> None:
|
|||||||
sa.Column(
|
sa.Column(
|
||||||
"status",
|
"status",
|
||||||
sa.Enum(
|
sa.Enum(
|
||||||
|
"backlog",
|
||||||
"pending",
|
"pending",
|
||||||
"claimed",
|
"claimed",
|
||||||
"in_progress",
|
"in_progress",
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ COPY --chown=agent:agent pyproject.toml uv.lock README.md /app/
|
|||||||
USER agent
|
USER agent
|
||||||
|
|
||||||
# Install Python dependencies for MCP servers (as 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
|
RUN uv python install 3.13 && uv sync --frozen --python 3.13
|
||||||
|
|
||||||
# Claude Code will use mounted ~/.claude for auth
|
# 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
|
COPY alembic /app/alembic
|
||||||
|
|
||||||
# Install Python dependencies
|
# 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
|
RUN uv python install 3.13 && uv sync --frozen --python 3.13
|
||||||
|
|
||||||
# Expose API port
|
# Expose API port
|
||||||
|
|||||||
@@ -556,6 +556,50 @@ class Agent(ABC):
|
|||||||
self.log.warning("Failed to read dev notes", error=str(e))
|
self.log.warning("Failed to read dev notes", error=str(e))
|
||||||
return "Dev notes unavailable"
|
return "Dev notes unavailable"
|
||||||
|
|
||||||
|
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]:
|
async def _get_task_commits(self, task_id: UUID) -> list[str]:
|
||||||
"""Get commits for the task."""
|
"""Get commits for the task."""
|
||||||
try:
|
try:
|
||||||
@@ -596,6 +640,26 @@ class Agent(ABC):
|
|||||||
"""Mark task as blocked."""
|
"""Mark task as blocked."""
|
||||||
await self._update_task_status(task_id, TaskStatus.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:
|
async def _mark_awaiting_qa(self, task_id: UUID) -> None:
|
||||||
"""Mark task as awaiting QA review."""
|
"""Mark task as awaiting QA review."""
|
||||||
await self._update_task_status(task_id, TaskStatus.AWAITING_QA)
|
await self._update_task_status(task_id, TaskStatus.AWAITING_QA)
|
||||||
|
|||||||
@@ -210,9 +210,23 @@ medium,TASK-abc123,P1,backend-dev-1
|
|||||||
- Answer questions
|
- Answer questions
|
||||||
- Clarify requirements
|
- Clarify requirements
|
||||||
- Remove small blockers
|
- Remove small blockers
|
||||||
|
- Unblock blocked tasks when blocker is resolved
|
||||||
"""
|
"""
|
||||||
self.log.debug("FACILITATE phase")
|
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
|
# Check for pending questions in channel
|
||||||
questions = await self._get_pending_questions()
|
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))
|
self.log.warning("Failed to get active tasks", error=str(e))
|
||||||
return []
|
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]:
|
async def _check_task_progress(self, task_id: UUID) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Check progress of a task.
|
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)
|
- Read dev's handoff notes (from task's dev_notes field)
|
||||||
- Review commits
|
- Review commits
|
||||||
- Check conversation history
|
- Check conversation history
|
||||||
|
- Read developer's journal entries for this task (if needed)
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
self.log.info("UNDERSTAND phase", task_id=str(ctx.task_id))
|
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)
|
dev_notes = await self._read_dev_notes(ctx.task_id)
|
||||||
commits = await self._get_task_commits_formatted(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
|
# Use TOON for token-efficient context encoding
|
||||||
task_context = self._format_review_context(
|
task_context = self._format_review_context(
|
||||||
ctx.title, requirements, dev_notes, commits
|
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.
|
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.
|
Based on this, create test cases to verify the implementation.
|
||||||
|
|
||||||
@@ -225,11 +230,8 @@ Focus on:
|
|||||||
- Integration points
|
- Integration points
|
||||||
- Error handling
|
- Error handling
|
||||||
|
|
||||||
IMPORTANT: Journals are PRIVATE and personal to each agent. You cannot read
|
If acceptance criteria mentions journaling requirements, verify them against
|
||||||
another agent's journal entries. If acceptance criteria mentions journaling:
|
the developer journal entries provided above.
|
||||||
- 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
|
|
||||||
|
|
||||||
Format response as TOON tabular:
|
Format response as TOON tabular:
|
||||||
[N,]{{name,description,steps,expected}}:
|
[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 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
|
# AGENT ROLE MAPPINGS
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -120,17 +133,19 @@ ESCALATION_CHAIN: Final[dict[str, str]] = {
|
|||||||
|
|
||||||
|
|
||||||
def get_agent_role(agent_id: str) -> str:
|
def get_agent_role(agent_id: str) -> str:
|
||||||
"""Get the role for an agent."""
|
"""Get the role for an agent. Accepts both UUID and slug."""
|
||||||
return AGENT_ROLE_MAP.get(agent_id, "unknown")
|
slug = _resolve_to_slug(agent_id)
|
||||||
|
return AGENT_ROLE_MAP.get(slug, "unknown")
|
||||||
|
|
||||||
|
|
||||||
def get_agent_team(agent_id: str) -> str | None:
|
def get_agent_team(agent_id: str) -> str | None:
|
||||||
"""Get the team for an agent."""
|
"""Get the team for an agent. Accepts both UUID and slug."""
|
||||||
return AGENT_TEAM_MAP.get(agent_id)
|
slug = _resolve_to_slug(agent_id)
|
||||||
|
return AGENT_TEAM_MAP.get(slug)
|
||||||
|
|
||||||
|
|
||||||
def get_agent_cell(agent_id: str) -> str | None:
|
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)
|
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.models import AgentRole, Team
|
||||||
from roboco.runtime import AgentOrchestrator
|
from roboco.runtime import AgentOrchestrator
|
||||||
from roboco.services.permissions import AgentContext, PermissionService
|
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:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable, Coroutine
|
from collections.abc import Callable, Coroutine
|
||||||
@@ -173,8 +173,26 @@ async def get_agent_context(
|
|||||||
detail="Missing X-Agent-Role header",
|
detail="Missing X-Agent-Role header",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Resolve agent ID (UUID or slug)
|
# Special case: system role (orchestrator) uses well-known UUID
|
||||||
agent_id = await resolve_agent_id(x_agent_id, db)
|
# 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:
|
try:
|
||||||
role = AgentRole(x_agent_role.lower())
|
role = AgentRole(x_agent_role.lower())
|
||||||
@@ -193,6 +211,7 @@ async def get_agent_context(
|
|||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
role=role,
|
role=role,
|
||||||
team=team,
|
team=team,
|
||||||
|
slug=slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ from roboco.api.schemas.journals import (
|
|||||||
StruggleRequest,
|
StruggleRequest,
|
||||||
TaskReflectionRequest,
|
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 (
|
from roboco.models.journal import (
|
||||||
DecisionLogParams,
|
DecisionLogParams,
|
||||||
GeneralEntryParams,
|
GeneralEntryParams,
|
||||||
@@ -513,17 +514,30 @@ async def get_entry(
|
|||||||
detail=f"Entry not found: {entry_id}",
|
detail=f"Entry not found: {entry_id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check privacy (simplified - in production would check journal ownership)
|
# Get journal to check ownership
|
||||||
if entry.is_private:
|
journal = await service.get_journal(entry.journal_id)
|
||||||
journal = await service.get_journal(entry.journal_id)
|
if not journal:
|
||||||
# Allow CEO and Auditor to see private entries
|
raise HTTPException(
|
||||||
is_other_agent = journal and journal.agent_id != agent.agent_id
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
is_unprivileged = agent.role not in [AgentRole.CEO, AgentRole.AUDITOR]
|
detail=f"Journal not found for entry: {entry_id}",
|
||||||
if is_other_agent and is_unprivileged:
|
)
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
# Get owner's slug for permission checking
|
||||||
detail="This entry is private",
|
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(
|
return JournalEntryResponse(
|
||||||
id=entry.id,
|
id=entry.id,
|
||||||
@@ -579,13 +593,13 @@ async def delete_entry(
|
|||||||
@router.get("/{agent_id}", response_model=JournalResponse)
|
@router.get("/{agent_id}", response_model=JournalResponse)
|
||||||
async def get_journal_by_agent(
|
async def get_journal_by_agent(
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
_agent: CurrentAgentContext,
|
agent: CurrentAgentContext,
|
||||||
db: DbSession,
|
db: DbSession,
|
||||||
) -> JournalResponse:
|
) -> JournalResponse:
|
||||||
"""
|
"""
|
||||||
Get a journal by agent ID (UUID) or slug (e.g., "be-dev-1").
|
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)
|
service = get_journal_service(db)
|
||||||
|
|
||||||
@@ -597,6 +611,23 @@ async def get_journal_by_agent(
|
|||||||
detail=f"Agent not found: {agent_id}",
|
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)
|
journal = await service.get_journal_by_agent(resolved_id)
|
||||||
if not journal:
|
if not journal:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -620,14 +651,15 @@ async def get_journal_by_agent(
|
|||||||
@router.get("/{agent_id}/entries", response_model=list[JournalEntryResponse])
|
@router.get("/{agent_id}/entries", response_model=list[JournalEntryResponse])
|
||||||
async def list_agent_entries(
|
async def list_agent_entries(
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
_agent: CurrentAgentContext,
|
agent: CurrentAgentContext,
|
||||||
db: DbSession,
|
db: DbSession,
|
||||||
params: Annotated[ListEntriesParams, Depends()],
|
params: Annotated[ListEntriesParams, Depends()],
|
||||||
) -> list[JournalEntryResponse]:
|
) -> list[JournalEntryResponse]:
|
||||||
"""
|
"""
|
||||||
List journal entries for a specific agent by UUID or slug.
|
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)
|
service = get_journal_service(db)
|
||||||
|
|
||||||
@@ -639,6 +671,23 @@ async def list_agent_entries(
|
|||||||
detail=f"Agent not found: {agent_id}",
|
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)
|
journal = await service.get_journal_by_agent(resolved_id)
|
||||||
if not journal:
|
if not journal:
|
||||||
return []
|
return []
|
||||||
@@ -653,6 +702,7 @@ async def list_agent_entries(
|
|||||||
detail=f"Invalid entry type: {e}",
|
detail=f"Invalid entry type: {e}",
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
# Cell members with access can see all entries including private
|
||||||
entries = await service.list_entries(
|
entries = await service.list_entries(
|
||||||
journal_id=journal.id,
|
journal_id=journal.id,
|
||||||
filters=ListEntriesFilter(
|
filters=ListEntriesFilter(
|
||||||
@@ -660,7 +710,7 @@ async def list_agent_entries(
|
|||||||
task_id=params.task_id,
|
task_id=params.task_id,
|
||||||
limit=params.limit,
|
limit=params.limit,
|
||||||
offset=params.offset,
|
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:
|
Enforces permission rules:
|
||||||
- Only PMs, Board members, and Auditor can send notifications
|
- 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
|
- Main PM, Auditor, and CEO can notify anyone
|
||||||
"""
|
"""
|
||||||
# Look up the sending agent to get their agent_id string
|
# Look up the sending agent to get their agent_id string
|
||||||
@@ -121,21 +121,21 @@ async def send_notification(
|
|||||||
detail="Agent not found",
|
detail="Agent not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Look up recipient agent_ids
|
# Look up recipient slugs for permission checking
|
||||||
recipient_ids = []
|
recipient_slugs = []
|
||||||
for recipient_uuid in data.to_agents:
|
for recipient_uuid in data.to_agents:
|
||||||
recipient_result = await db.execute(
|
recipient_result = await db.execute(
|
||||||
select(AgentTable).where(AgentTable.id == recipient_uuid)
|
select(AgentTable).where(AgentTable.id == recipient_uuid)
|
||||||
)
|
)
|
||||||
recipient = recipient_result.scalar_one_or_none()
|
recipient = recipient_result.scalar_one_or_none()
|
||||||
if recipient:
|
if recipient:
|
||||||
recipient_ids.append(str(recipient.id))
|
recipient_slugs.append(recipient.slug)
|
||||||
|
|
||||||
# Validate notification permissions using enforcement layer
|
# Validate notification permissions using enforcement layer
|
||||||
try:
|
try:
|
||||||
validate_notification_permission(
|
validate_notification_permission(
|
||||||
sender_id=agent.slug,
|
sender_id=agent.slug,
|
||||||
recipients=recipient_ids,
|
recipients=recipient_slugs,
|
||||||
)
|
)
|
||||||
except NotificationPermissionError as e:
|
except NotificationPermissionError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -378,6 +378,7 @@ async def create_session_for_tasks(
|
|||||||
req = SessionForTasksCreate(
|
req = SessionForTasksCreate(
|
||||||
task_ids=data.task_ids,
|
task_ids=data.task_ids,
|
||||||
channel_slug=data.channel_slug,
|
channel_slug=data.channel_slug,
|
||||||
|
scope=data.scope,
|
||||||
relationship_type=rel_type,
|
relationship_type=rel_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -388,11 +389,22 @@ async def create_session_for_tasks(
|
|||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=str(e),
|
detail=str(e),
|
||||||
) from e
|
) from e
|
||||||
|
except ConflictError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail=str(e),
|
||||||
|
) from e
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=str(e),
|
detail=str(e),
|
||||||
) from 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(
|
session_response = SessionResponse(
|
||||||
id=require_uuid(session.id),
|
id=require_uuid(session.id),
|
||||||
|
|||||||
+15
-12
@@ -14,6 +14,7 @@ from roboco.api.deps import (
|
|||||||
CurrentAgentContext,
|
CurrentAgentContext,
|
||||||
DbSession,
|
DbSession,
|
||||||
PermissionServiceDep,
|
PermissionServiceDep,
|
||||||
|
get_permission_service,
|
||||||
)
|
)
|
||||||
from roboco.api.schemas.sessions import (
|
from roboco.api.schemas.sessions import (
|
||||||
SessionTaskLinkResponse,
|
SessionTaskLinkResponse,
|
||||||
@@ -23,7 +24,6 @@ from roboco.api.schemas.tasks import (
|
|||||||
CheckpointRequest,
|
CheckpointRequest,
|
||||||
ClaimRequest,
|
ClaimRequest,
|
||||||
CommitRequest,
|
CommitRequest,
|
||||||
ListTasksQuery,
|
|
||||||
ProgressRequest,
|
ProgressRequest,
|
||||||
QANotes,
|
QANotes,
|
||||||
SoftBlockRequest,
|
SoftBlockRequest,
|
||||||
@@ -92,6 +92,7 @@ async def create_task(
|
|||||||
parent_task_id=data.parent_task_id,
|
parent_task_id=data.parent_task_id,
|
||||||
target_date=data.target_date,
|
target_date=data.target_date,
|
||||||
estimated_complexity=data.estimated_complexity,
|
estimated_complexity=data.estimated_complexity,
|
||||||
|
status=data.status,
|
||||||
)
|
)
|
||||||
task = await service.create(req)
|
task = await service.create(req)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -102,8 +103,9 @@ async def create_task(
|
|||||||
async def list_tasks(
|
async def list_tasks(
|
||||||
db: DbSession,
|
db: DbSession,
|
||||||
agent: CurrentAgentContext,
|
agent: CurrentAgentContext,
|
||||||
permissions: PermissionServiceDep,
|
team: Team | None = None,
|
||||||
params: Annotated[ListTasksQuery, Query()],
|
status: TaskStatus | None = None,
|
||||||
|
limit: int = Query(100, ge=1, le=500),
|
||||||
) -> list[TaskResponse]:
|
) -> list[TaskResponse]:
|
||||||
"""
|
"""
|
||||||
List tasks with optional filters.
|
List tasks with optional filters.
|
||||||
@@ -114,10 +116,11 @@ async def list_tasks(
|
|||||||
- Cell members: Can only see own cell's tasks
|
- Cell members: Can only see own cell's tasks
|
||||||
"""
|
"""
|
||||||
service = get_task_service(db)
|
service = get_task_service(db)
|
||||||
|
permissions = get_permission_service()
|
||||||
|
|
||||||
# Determine effective team filter based on permissions
|
# Determine effective team filter based on permissions
|
||||||
can_view_all = permissions.can_perform_task_action(agent, TaskAction.VIEW_ALL)
|
can_view_all = permissions.can_perform_task_action(agent, TaskAction.VIEW_ALL)
|
||||||
effective_team = params.team
|
effective_team = team
|
||||||
|
|
||||||
if not can_view_all:
|
if not can_view_all:
|
||||||
# Cell members can only see their own team's tasks
|
# Cell members can only see their own team's tasks
|
||||||
@@ -127,14 +130,14 @@ async def list_tasks(
|
|||||||
# No team assigned - return empty list
|
# No team assigned - return empty list
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if effective_team and params.status:
|
if effective_team and status:
|
||||||
tasks = await service.list_by_team(effective_team, params.status, params.limit)
|
tasks = await service.list_by_team(effective_team, status, limit)
|
||||||
elif effective_team:
|
elif effective_team:
|
||||||
tasks = await service.list_by_team(effective_team, limit=params.limit)
|
tasks = await service.list_by_team(effective_team, limit=limit)
|
||||||
elif params.status:
|
elif status:
|
||||||
tasks = await service.list_by_status(params.status)
|
tasks = await service.list_by_status(status)
|
||||||
else:
|
else:
|
||||||
tasks = await service.list_all(params.limit, params.offset)
|
tasks = await service.list_all(limit)
|
||||||
|
|
||||||
return task_list_to_response(tasks)
|
return task_list_to_response(tasks)
|
||||||
|
|
||||||
@@ -1114,8 +1117,8 @@ async def activate_task(
|
|||||||
|
|
||||||
REQUIRES: Task must have at least one linked session.
|
REQUIRES: Task must have at least one linked session.
|
||||||
"""
|
"""
|
||||||
# Check PM permission
|
# Check PM permission (CREATE permission required for activation)
|
||||||
if not permissions.can_perform_task_action(agent, "create_tasks"):
|
if not permissions.can_perform_task_action(agent, TaskAction.CREATE):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Only PMs and management can activate tasks",
|
detail="Only PMs and management can activate tasks",
|
||||||
|
|||||||
+1
-1
@@ -507,7 +507,7 @@ class SessionTaskTable(Base):
|
|||||||
"ix_session_tasks_primary_per_task",
|
"ix_session_tasks_primary_per_task",
|
||||||
"task_id",
|
"task_id",
|
||||||
unique=True,
|
unique=True,
|
||||||
postgresql_where=(is_primary == True), # noqa: E712
|
postgresql_where=(is_primary.is_(True)),
|
||||||
),
|
),
|
||||||
# Fast lookups
|
# Fast lookups
|
||||||
Index("ix_session_tasks_task_id", "task_id"),
|
Index("ix_session_tasks_task_id", "task_id"),
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ from roboco.enforcement.channel_access import (
|
|||||||
get_agent_channels,
|
get_agent_channels,
|
||||||
validate_channel_access,
|
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 (
|
from roboco.enforcement.notification_perms import (
|
||||||
NotificationPermissionError,
|
NotificationPermissionError,
|
||||||
get_notification_scope,
|
get_notification_scope,
|
||||||
@@ -46,19 +52,23 @@ __all__ = [
|
|||||||
"ROLE_RESTRICTED_TRANSITIONS",
|
"ROLE_RESTRICTED_TRANSITIONS",
|
||||||
"VALID_TRANSITIONS",
|
"VALID_TRANSITIONS",
|
||||||
"ChannelAccessDeniedError",
|
"ChannelAccessDeniedError",
|
||||||
|
"JournalAccessDeniedError",
|
||||||
"NotificationPermissionError",
|
"NotificationPermissionError",
|
||||||
"TaskClaimContext",
|
"TaskClaimContext",
|
||||||
"TaskLifecycleError",
|
"TaskLifecycleError",
|
||||||
"TaskOwnershipError",
|
"TaskOwnershipError",
|
||||||
"can_agent_transition",
|
"can_agent_transition",
|
||||||
|
"can_read_journal",
|
||||||
"can_review_task",
|
"can_review_task",
|
||||||
"get_agent_channels",
|
"get_agent_channels",
|
||||||
"get_notification_scope",
|
"get_notification_scope",
|
||||||
|
"get_readable_journals",
|
||||||
"get_valid_transitions",
|
"get_valid_transitions",
|
||||||
"is_active_state",
|
"is_active_state",
|
||||||
"is_terminal_state",
|
"is_terminal_state",
|
||||||
"is_waiting_state",
|
"is_waiting_state",
|
||||||
"validate_channel_access",
|
"validate_channel_access",
|
||||||
|
"validate_journal_access",
|
||||||
"validate_notification_permission",
|
"validate_notification_permission",
|
||||||
"validate_task_claim",
|
"validate_task_claim",
|
||||||
"validate_task_ownership",
|
"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)
|
role = get_agent_role(sender_id)
|
||||||
permissions = NOTIFICATION_PERMISSIONS.get(role, {"can_send": False})
|
permissions = NOTIFICATION_PERMISSIONS.get(role, {"can_send": False})
|
||||||
|
|
||||||
|
can_send = False
|
||||||
|
reason = ""
|
||||||
|
|
||||||
if not permissions.get("can_send", False):
|
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":
|
if (sender_cell and sender_cell == recipient_cell) or recipient_role in {
|
||||||
return True, "OK"
|
"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":
|
return can_send, reason
|
||||||
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}"
|
|
||||||
|
|
||||||
|
|
||||||
def validate_notification_permission(
|
def validate_notification_permission(
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from typing import Any
|
|||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
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.llm import ToonAdapter
|
||||||
from roboco.mcp.schemas import (
|
from roboco.mcp.schemas import (
|
||||||
DecisionLogInput,
|
DecisionLogInput,
|
||||||
@@ -57,6 +59,7 @@ async def _handle_journal_entry(
|
|||||||
"title": data.title,
|
"title": data.title,
|
||||||
"content": data.content,
|
"content": data.content,
|
||||||
"task_id": data.task_id,
|
"task_id": data.task_id,
|
||||||
|
"session_id": data.session_id,
|
||||||
"tags": data.tags,
|
"tags": data.tags,
|
||||||
"is_private": data.is_private,
|
"is_private": data.is_private,
|
||||||
}
|
}
|
||||||
@@ -86,6 +89,7 @@ async def _handle_reflect(
|
|||||||
"""Handle task reflection creation."""
|
"""Handle task reflection creation."""
|
||||||
payload = {
|
payload = {
|
||||||
"task_id": data.task_id,
|
"task_id": data.task_id,
|
||||||
|
"session_id": data.session_id,
|
||||||
"title": data.title,
|
"title": data.title,
|
||||||
"what_done": data.what_done,
|
"what_done": data.what_done,
|
||||||
"what_learned": data.what_learned,
|
"what_learned": data.what_learned,
|
||||||
@@ -283,6 +287,50 @@ async def _handle_recent(
|
|||||||
return {"entries": entries, "count": len(entries) if entries else 0}
|
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
|
# 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)
|
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
|
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)
|
role = get_agent_role(sender_id)
|
||||||
permissions = NOTIFICATION_PERMISSIONS.get(role, {"can_send": False})
|
permissions = NOTIFICATION_PERMISSIONS.get(role, {"can_send": False})
|
||||||
|
|
||||||
|
can_send = False
|
||||||
|
reason = ""
|
||||||
|
|
||||||
if not permissions.get("can_send", False):
|
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":
|
# Cell PM can notify their own cell members
|
||||||
return True, "OK"
|
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":
|
return can_send, reason
|
||||||
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}"
|
|
||||||
|
|
||||||
|
|
||||||
# Valid notification types and priorities
|
# Valid notification types and priorities
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class JournalEntryInput(BaseModel):
|
|||||||
description="Type: general, task_reflection, decision_log, learning, struggle",
|
description="Type: general, task_reflection, decision_log, learning, struggle",
|
||||||
)
|
)
|
||||||
task_id: str | None = Field(default=None, description="Optional related task")
|
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")
|
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
|
||||||
is_private: bool = Field(
|
is_private: bool = Field(
|
||||||
default=False, description="If true, only you and CEO/Auditor can see"
|
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."""
|
"""Input for creating a task reflection entry."""
|
||||||
|
|
||||||
task_id: str = Field(..., description="The task UUID you're reflecting on")
|
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")
|
title: str = Field(..., description="Reflection title")
|
||||||
what_done: str = Field(..., description="What was accomplished")
|
what_done: str = Field(..., description="What was accomplished")
|
||||||
what_learned: str = Field(..., description="Key learnings from this task")
|
what_learned: str = Field(..., description="Key learnings from this task")
|
||||||
@@ -177,6 +179,10 @@ class TaskCreateInput(BaseModel):
|
|||||||
complexity: str = Field(
|
complexity: str = Field(
|
||||||
default="medium", description="Complexity: low, medium, high, critical"
|
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):
|
class TaskAssignInput(BaseModel):
|
||||||
|
|||||||
+73
-53
@@ -72,20 +72,8 @@ from roboco.mcp.tasks.handlers import (
|
|||||||
from roboco.mcp.utils import ApiClient
|
from roboco.mcp.utils import ApiClient
|
||||||
|
|
||||||
|
|
||||||
def create_task_mcp_server(agent_id: str) -> FastMCP: # noqa: PLR0915
|
def _register_core_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||||
"""
|
"""Register core task lifecycle tools."""
|
||||||
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)
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_task_scan(team: str | None = None) -> dict[str, Any]:
|
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
|
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()
|
@mcp.tool()
|
||||||
async def roboco_task_block(
|
async def roboco_task_block(
|
||||||
task_id: str, reason: str, blocker_type: str, what_needed: str
|
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)
|
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()
|
@mcp.tool()
|
||||||
async def roboco_task_submit_verification(task_id: str) -> dict[str, Any]:
|
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)
|
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.
|
def _register_pm_tools(mcp: FastMCP, client: ApiClient, agent_id: str) -> None:
|
||||||
Your container will be terminated to save resources.
|
"""Register PM delegation and management tools."""
|
||||||
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
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def roboco_task_create(data: TaskCreateInput) -> dict[str, Any]:
|
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:
|
Args:
|
||||||
data: TaskCreateInput with title, description, acceptance_criteria,
|
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:
|
Returns:
|
||||||
Created task with next step guidance
|
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)
|
return await handle_task_escalate(client, input_data, agent_id)
|
||||||
|
|
||||||
# =========================================================================
|
@mcp.tool()
|
||||||
# PM SESSION TOOLS
|
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()
|
@mcp.tool()
|
||||||
async def roboco_session_create_for_tasks(
|
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)
|
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
|
def create_task_mcp_server(agent_id: str) -> FastMCP:
|
||||||
a task, you MUST:
|
"""
|
||||||
1. Create a session: roboco_session_create_for_tasks()
|
Create a Task MCP server for a specific agent.
|
||||||
2. Activate the task: roboco_task_activate()
|
|
||||||
|
|
||||||
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:
|
Args:
|
||||||
- Only PMs and management can activate tasks
|
agent_id: The agent identifier (e.g., "be-dev-1")
|
||||||
- Task must be in BACKLOG status
|
|
||||||
- Task MUST have at least one linked session
|
|
||||||
|
|
||||||
Args:
|
Returns:
|
||||||
task_id: The task UUID to activate
|
Configured FastMCP server
|
||||||
|
"""
|
||||||
|
mcp = FastMCP(f"roboco-task-{agent_id}", json_response=True)
|
||||||
|
client = ApiClient(agent_id)
|
||||||
|
|
||||||
Returns:
|
# Register all tools via helper functions
|
||||||
Activated task with PENDING status
|
_register_core_tools(mcp, client, agent_id)
|
||||||
"""
|
_register_blocking_tools(mcp, client, agent_id)
|
||||||
return await handle_task_activate(client, task_id, 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
|
return mcp
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from typing import Any
|
|||||||
|
|
||||||
from fastapi import status
|
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.schemas import TaskBlockInput, TaskPauseInput
|
||||||
from roboco.mcp.tasks import format_task_response
|
from roboco.mcp.tasks import format_task_response
|
||||||
from roboco.mcp.tasks.handlers._helpers import validate_task_ownership
|
from roboco.mcp.tasks.handlers._helpers import validate_task_ownership
|
||||||
@@ -50,16 +51,34 @@ async def handle_task_block(
|
|||||||
|
|
||||||
return format_task_response(
|
return format_task_response(
|
||||||
block_resp.json(),
|
block_resp.json(),
|
||||||
"WAIT_OR_SWITCH",
|
"RESOLVE_BLOCKER",
|
||||||
f"Task blocked: {data.reason}\n\n"
|
f"Task blocked: {data.reason}\n\n"
|
||||||
"Options:\n"
|
"Options:\n"
|
||||||
"1. WAIT - If resolution expected soon, poll for updates\n"
|
"1. UNBLOCK - When resolved, call roboco_task_unblock() to resume\n"
|
||||||
"2. SWITCH - Call roboco_task_scan to work on another task\n"
|
"2. WAIT - If waiting for external resolution\n"
|
||||||
"3. ESCALATE - Message your PM if this is urgent\n\n"
|
"3. SWITCH - Call roboco_task_scan for other work\n"
|
||||||
"The blocker has been communicated. You'll be notified when resolved.",
|
"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(
|
async def handle_task_unblock(
|
||||||
client: ApiClient, task_id: str, agent_id: str
|
client: ApiClient, task_id: str, agent_id: str
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -70,12 +89,17 @@ async def handle_task_unblock(
|
|||||||
|
|
||||||
task = task_resp.json()
|
task = task_resp.json()
|
||||||
|
|
||||||
if error := await validate_task_ownership(task, agent_id, client):
|
|
||||||
return error
|
|
||||||
|
|
||||||
if task.get("status") != "blocked":
|
if task.get("status") != "blocked":
|
||||||
return format_error_response("INVALID_STATE", "Task is not 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")
|
unblock_resp = await client.post(f"/tasks/{task_id}/unblock")
|
||||||
|
|
||||||
if not unblock_resp.ok:
|
if not unblock_resp.ok:
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ def _build_task_payload(input_data: TaskCreateInput) -> dict[str, Any]:
|
|||||||
"team": input_data.team,
|
"team": input_data.team,
|
||||||
"priority": input_data.priority,
|
"priority": input_data.priority,
|
||||||
"estimated_complexity": input_data.complexity,
|
"estimated_complexity": input_data.complexity,
|
||||||
|
"status": input_data.status, # Always included, defaults to "backlog"
|
||||||
}
|
}
|
||||||
if input_data.parent_task_id:
|
if input_data.parent_task_id:
|
||||||
payload["parent_task_id"] = 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:
|
def _format_create_guidance(task: dict[str, Any], assigned_to: str | None) -> str:
|
||||||
"""Format guidance message for task creation."""
|
"""Format guidance message for task creation."""
|
||||||
guidance = f"Task created successfully. ID: {task['id']}. "
|
task_status = task.get("status", "pending")
|
||||||
if assigned_to:
|
guidance = f"Task created successfully. ID: {task['id']}. Status: {task_status}. "
|
||||||
|
|
||||||
|
if task_status == "backlog":
|
||||||
guidance += (
|
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."
|
"Orchestrator will spawn them to claim and work on it."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -62,11 +62,12 @@ def get_next_step_guidance(status: str) -> tuple[str, str]:
|
|||||||
"If blocked, call roboco_task_block with details.",
|
"If blocked, call roboco_task_block with details.",
|
||||||
),
|
),
|
||||||
"blocked": (
|
"blocked": (
|
||||||
"WAIT_OR_SWITCH",
|
"RESOLVE_BLOCKER",
|
||||||
"You are blocked. Options: "
|
"Task is blocked. Options: "
|
||||||
"1) Wait for resolution (if expected soon), "
|
"1) UNBLOCK - If resolved, call roboco_task_unblock() to resume. "
|
||||||
"2) Switch to another task (call roboco_task_scan), "
|
"2) WAIT - If waiting for external resolution. "
|
||||||
"3) Escalate to PM if urgent.",
|
"3) SWITCH - Call roboco_task_scan for other work. "
|
||||||
|
"4) ESCALATE - If urgent, message your PM.",
|
||||||
),
|
),
|
||||||
"paused": (
|
"paused": (
|
||||||
"RESUME_OR_SCAN",
|
"RESUME_OR_SCAN",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class AgentContext:
|
|||||||
agent_id: UUID
|
agent_id: UUID
|
||||||
role: AgentRole
|
role: AgentRole
|
||||||
team: Team | None = None
|
team: Team | None = None
|
||||||
|
slug: str | None = None # Agent slug (e.g., "be-dev-1")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def level(self) -> PermissionLevel:
|
def level(self) -> PermissionLevel:
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ class TaskCreate(RobocoBase):
|
|||||||
parent_task_id: UUID | None = None
|
parent_task_id: UUID | None = None
|
||||||
target_date: datetime | None = None
|
target_date: datetime | None = None
|
||||||
estimated_complexity: Complexity = Complexity.MEDIUM
|
estimated_complexity: Complexity = Complexity.MEDIUM
|
||||||
|
status: TaskStatus | None = None # PM can set 'backlog' for subtasks needing setup
|
||||||
|
|
||||||
|
|
||||||
class TaskUpdate(RobocoBase):
|
class TaskUpdate(RobocoBase):
|
||||||
@@ -270,3 +271,4 @@ class TaskCreateRequest:
|
|||||||
parent_task_id: UUID | None = None
|
parent_task_id: UUID | None = None
|
||||||
target_date: datetime | None = None
|
target_date: datetime | None = None
|
||||||
estimated_complexity: Complexity = field(default=Complexity.MEDIUM)
|
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)
|
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
|
# JOURNAL CRUD
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
@@ -452,11 +452,14 @@ class MessagingService(BaseService):
|
|||||||
if existing_primary.scalar_one_or_none():
|
if existing_primary.scalar_one_or_none():
|
||||||
raise ConflictError(f"Task {task_id} already has a primary session")
|
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(
|
link = SessionTaskTable(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
is_primary=is_primary,
|
is_primary=is_primary,
|
||||||
relationship_type=relationship_type.value,
|
relationship_type=rel_value,
|
||||||
added_by=added_by,
|
added_by=added_by,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -468,7 +471,7 @@ class MessagingService(BaseService):
|
|||||||
session_id=str(session_id),
|
session_id=str(session_id),
|
||||||
task_id=str(task_id),
|
task_id=str(task_id),
|
||||||
is_primary=is_primary,
|
is_primary=is_primary,
|
||||||
relationship_type=relationship_type.value,
|
relationship_type=rel_value,
|
||||||
)
|
)
|
||||||
return link
|
return link
|
||||||
|
|
||||||
@@ -534,9 +537,8 @@ class MessagingService(BaseService):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if relationship_type:
|
if relationship_type:
|
||||||
query = query.where(
|
rel_value = getattr(relationship_type, "value", relationship_type)
|
||||||
SessionTaskTable.relationship_type == relationship_type.value
|
query = query.where(SessionTaskTable.relationship_type == rel_value)
|
||||||
)
|
|
||||||
|
|
||||||
query = query.order_by(SessionTaskTable.added_at.desc())
|
query = query.order_by(SessionTaskTable.added_at.desc())
|
||||||
result = await self.session.execute(query)
|
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.base import BaseRepository
|
||||||
from roboco.services.repositories.query_helpers import (
|
from roboco.services.repositories.query_helpers import (
|
||||||
agent_id_filter,
|
agent_id_filter,
|
||||||
|
get_agent_slug,
|
||||||
pagination,
|
pagination,
|
||||||
|
resolve_agent_identity,
|
||||||
resolve_agent_uuid,
|
resolve_agent_uuid,
|
||||||
status_filter,
|
status_filter,
|
||||||
team_filter,
|
team_filter,
|
||||||
@@ -18,7 +20,9 @@ from roboco.services.repositories.query_helpers import (
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"BaseRepository",
|
"BaseRepository",
|
||||||
"agent_id_filter",
|
"agent_id_filter",
|
||||||
|
"get_agent_slug",
|
||||||
"pagination",
|
"pagination",
|
||||||
|
"resolve_agent_identity",
|
||||||
"resolve_agent_uuid",
|
"resolve_agent_uuid",
|
||||||
"status_filter",
|
"status_filter",
|
||||||
"team_filter",
|
"team_filter",
|
||||||
|
|||||||
@@ -188,6 +188,66 @@ async def resolve_agent_uuid(
|
|||||||
return UUID(str(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(
|
async def get_agent_by_slug(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
slug: str,
|
slug: str,
|
||||||
|
|||||||
@@ -118,11 +118,8 @@ class TaskService(BaseService):
|
|||||||
"""
|
"""
|
||||||
Create a new task.
|
Create a new task.
|
||||||
|
|
||||||
Tasks are created with BACKLOG status by default. PM must:
|
Default status is PENDING. PM can pass status=BACKLOG when creating
|
||||||
1. Create a session for the task
|
subtasks that need session setup before activation.
|
||||||
2. Call activate() to transition to PENDING
|
|
||||||
|
|
||||||
This ensures every task has a session before work begins.
|
|
||||||
"""
|
"""
|
||||||
task = TaskTable(
|
task = TaskTable(
|
||||||
title=req.title,
|
title=req.title,
|
||||||
@@ -134,7 +131,7 @@ class TaskService(BaseService):
|
|||||||
parent_task_id=req.parent_task_id,
|
parent_task_id=req.parent_task_id,
|
||||||
target_date=req.target_date,
|
target_date=req.target_date,
|
||||||
estimated_complexity=req.estimated_complexity,
|
estimated_complexity=req.estimated_complexity,
|
||||||
status=TaskStatus.BACKLOG,
|
status=req.status if req.status else TaskStatus.PENDING,
|
||||||
)
|
)
|
||||||
self.session.add(task)
|
self.session.add(task)
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
|
|||||||
Reference in New Issue
Block a user