Improved workflow. Ready for git, figma, etc; AND BIGGER TESTS

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