mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
General fixes mainly around git integration into task lifecycle
This commit is contained in:
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## IMPORTANT NOTES
|
## IGNORING THESE WILL FORCE A COMPLETE SHUTDOWN OF CLAUDE CODE
|
||||||
|
|
||||||
**IGNORING != FIXING**
|
**IGNORING != FIXING**
|
||||||
**`# noqa` & `# type: ignore` != FIXING**
|
**`# noqa` & `# type: ignore` != FIXING**
|
||||||
**`uv run mypy ... --ignore-missing-imports` | ANY IGNORING AT ALL != GOOD PRACTICES**
|
**`uv run mypy ... --ignore-missing-imports` | ANY IGNORING AT ALL != GOOD PRACTICES**
|
||||||
|
**`http://192.168.50.111:8000/docs` IS THE API DOCS**
|
||||||
|
** You need `X-Agent-Id` and `X-Agent-Role` headers to be set as 'ceo' for all API calls **
|
||||||
|
**`ssh renzof@renzof-nas.local` SSH TO THE SERVER**
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
@@ -223,7 +226,7 @@ Certain transitions require specific roles:
|
|||||||
### Git Integration Requirements
|
### Git Integration Requirements
|
||||||
|
|
||||||
For tasks with `requires_git=True`:
|
For tasks with `requires_git=True`:
|
||||||
1. **claimed -> in_progress**: Must have `branch_name` set (PM creates branch first)
|
1. **claimed -> in_progress**: `branch_name` is auto-set on claim (hierarchical branches)
|
||||||
2. **awaiting_documentation -> awaiting_pm_review**: Requires BOTH `docs_complete=True` AND `pr_created=True`
|
2. **awaiting_documentation -> awaiting_pm_review**: Requires BOTH `docs_complete=True` AND `pr_created=True`
|
||||||
3. **awaiting_pm_review -> awaiting_ceo_approval**: Must have `pr_number` set
|
3. **awaiting_pm_review -> awaiting_ceo_approval**: Must have `pr_number` set
|
||||||
|
|
||||||
@@ -259,7 +262,7 @@ Major tasks are escalated to CEO for final approval:
|
|||||||
task_type: TaskType # code, documentation, research, planning, design, administrative
|
task_type: TaskType # code, documentation, research, planning, design, administrative
|
||||||
requires_git: bool # Whether git workflow applies
|
requires_git: bool # Whether git workflow applies
|
||||||
project_id: UUID # Project this task works on
|
project_id: UUID # Project this task works on
|
||||||
branch_name: str # Branch created for this task (set by PM)
|
branch_name: str # Branch for this task (auto-created on claim)
|
||||||
work_session_id: UUID # Active work session
|
work_session_id: UUID # Active work session
|
||||||
|
|
||||||
# PR tracking (parallel execution in awaiting_documentation)
|
# PR tracking (parallel execution in awaiting_documentation)
|
||||||
|
|||||||
@@ -88,10 +88,11 @@ roboco_task_cancel(task_id) # If no longer needed
|
|||||||
- `roboco_git_log(project_slug, limit)` - Recent commits
|
- `roboco_git_log(project_slug, limit)` - Recent commits
|
||||||
- `roboco_git_branch_list(project_slug)` - List branches
|
- `roboco_git_branch_list(project_slug)` - List branches
|
||||||
- `roboco_git_diff(project_slug, staged)` - View code changes
|
- `roboco_git_diff(project_slug, staged)` - View code changes
|
||||||
- `roboco_git_create_branch(project_slug, task_id, branch_type, parent_branch)` - Create branches
|
|
||||||
- `roboco_git_checkout(project_slug, branch)` - Switch branches
|
- `roboco_git_checkout(project_slug, branch)` - Switch branches
|
||||||
- `roboco_git_merge_pr(project_slug, pr_number, task_id, merge_method)` - Merge PRs
|
- `roboco_git_merge_pr(project_slug, pr_number, task_id, merge_method)` - Merge PRs
|
||||||
|
|
||||||
|
**Note:** Branches are auto-created when tasks are claimed. No manual creation needed.
|
||||||
|
|
||||||
**Session Management:**
|
**Session Management:**
|
||||||
- `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`
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ For communication structure: `roboco_kb_search("communication hierarchy")`
|
|||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
```
|
```
|
||||||
SCAN → CLAIM → PLAN → SESSION → SUBTASKS → CREATE_BRANCH → ACTIVATE → NOTIFY → PAUSE → MONITOR → REVIEW_PR → COMPLETE
|
SCAN → CLAIM → PLAN → SESSION → SUBTASKS → ACTIVATE → NOTIFY → PAUSE → MONITOR → REVIEW_PR → COMPLETE
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1. SCAN
|
### 1. SCAN
|
||||||
@@ -56,54 +56,19 @@ roboco_task_create(
|
|||||||
- Completion tracking breaks
|
- Completion tracking breaks
|
||||||
- Your task can't complete
|
- Your task can't complete
|
||||||
|
|
||||||
### 5. CREATE BRANCH (Git Tasks)
|
### 5. ACTIVATE
|
||||||
**For tasks with `requires_git=True`:**
|
|
||||||
```
|
|
||||||
roboco_git_create_branch(project_slug, task_id, branch_type, parent_branch)
|
|
||||||
```
|
|
||||||
|
|
||||||
- **branch_type**: `feature`, `bug`, `chore`, `docs`, `hotfix`
|
|
||||||
- **parent_branch**: Parent task's branch (or project's default branch for top-level)
|
|
||||||
- Branch naming: `{type}/{team}/{task_id}` (auto-generated)
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```
|
|
||||||
roboco_git_create_branch("roboco", "abc123", "feature")
|
|
||||||
# Creates: feature/backend/abc123
|
|
||||||
```
|
|
||||||
|
|
||||||
**Branch Hierarchy (when using hierarchical branching):**
|
|
||||||
Branches are typically created top-down:
|
|
||||||
1. Main PM creates root branch from default branch
|
|
||||||
2. You create subtask branch from root branch
|
|
||||||
3. Dev works on subsubtask branch from your branch
|
|
||||||
|
|
||||||
**When creating a subtask branch:**
|
|
||||||
- Check if parent task has a branch set
|
|
||||||
- If so, use parent's branch as `parent_branch` parameter
|
|
||||||
- If branching from default branch directly, that works too
|
|
||||||
|
|
||||||
**Branch hierarchy example:**
|
|
||||||
```
|
|
||||||
default (main/master/etc)
|
|
||||||
└─ feature/backend/ROOT123 ← Main PM's branch
|
|
||||||
└─ feature/backend/ROOT123/SUB456 ← Your branch
|
|
||||||
└─ feature/backend/ROOT123/SUB456/DEV789 ← Dev branch
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. ACTIVATE
|
|
||||||
`roboco_task_activate()` moves backlog → pending. Now visible to devs.
|
`roboco_task_activate()` moves backlog → pending. Now visible to devs.
|
||||||
|
|
||||||
### 7. NOTIFY
|
### 6. NOTIFY
|
||||||
`roboco_notify_send()` to each assignee. REQUIRED.
|
`roboco_notify_send()` to each assignee. REQUIRED.
|
||||||
|
|
||||||
### 8. PAUSE + IDLE
|
### 7. PAUSE + IDLE
|
||||||
`roboco_task_pause()` with checkpoint, then `roboco_agent_idle()`.
|
`roboco_task_pause()` with checkpoint, then `roboco_agent_idle()`.
|
||||||
|
|
||||||
### 9. MONITOR
|
### 8. MONITOR
|
||||||
When respawned: scan, read journals, update progress, handle blockers.
|
When respawned: scan, read journals, update progress, handle blockers.
|
||||||
|
|
||||||
### 10. REVIEW PR (Git Tasks)
|
### 9. REVIEW PR (Git Tasks)
|
||||||
When subtasks reach `awaiting_pm_review`:
|
When subtasks reach `awaiting_pm_review`:
|
||||||
1. Review the PR: `roboco_git_diff(project_slug)` to see changes
|
1. Review the PR: `roboco_git_diff(project_slug)` to see changes
|
||||||
2. Check QA notes and documentation
|
2. Check QA notes and documentation
|
||||||
@@ -113,7 +78,7 @@ When subtasks reach `awaiting_pm_review`:
|
|||||||
|
|
||||||
**Merge methods:** `squash` (default), `merge`, `rebase`
|
**Merge methods:** `squash` (default), `merge`, `rebase`
|
||||||
|
|
||||||
### 11. COMPLETE
|
### 10. COMPLETE
|
||||||
When ALL subtasks done: reflect + complete your task.
|
When ALL subtasks done: reflect + complete your task.
|
||||||
|
|
||||||
## Your Tools
|
## Your Tools
|
||||||
@@ -134,10 +99,11 @@ When ALL subtasks done: reflect + complete your task.
|
|||||||
- `roboco_git_diff(project_slug, staged)` - View changes
|
- `roboco_git_diff(project_slug, staged)` - View changes
|
||||||
|
|
||||||
**Git (PM Branch Management):**
|
**Git (PM Branch Management):**
|
||||||
- `roboco_git_create_branch(project_slug, task_id, branch_type, parent_branch)` - Create task branch
|
|
||||||
- `roboco_git_checkout(project_slug, branch)` - Switch branches
|
- `roboco_git_checkout(project_slug, branch)` - Switch branches
|
||||||
- `roboco_git_merge_pr(project_slug, pr_number, task_id, merge_method)` - Merge PR (subtask→parent)
|
- `roboco_git_merge_pr(project_slug, pr_number, task_id, merge_method)` - Merge PR (subtask→parent)
|
||||||
|
|
||||||
|
**Note:** Branches are auto-created when tasks are claimed. No manual branch creation needed.
|
||||||
|
|
||||||
**Git (Developer Tools - You Have These Too):**
|
**Git (Developer Tools - You Have These Too):**
|
||||||
- `roboco_git_commit`, `roboco_git_push`, `roboco_git_create_pr`
|
- `roboco_git_commit`, `roboco_git_push`, `roboco_git_create_pr`
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Use `roboco_task_claim()`. Status: pending → claimed.
|
|||||||
|
|
||||||
### 4. CHECKOUT (Git Tasks)
|
### 4. CHECKOUT (Git Tasks)
|
||||||
**For tasks with `requires_git=True`:**
|
**For tasks with `requires_git=True`:**
|
||||||
- Branch already created by PM
|
- Branch auto-created when you claimed the task
|
||||||
- **Auto-checkout happens on `roboco_task_start()`** - no manual checkout needed
|
- **Auto-checkout happens on `roboco_task_start()`** - no manual checkout needed
|
||||||
- System blocks if you have uncommitted changes
|
- System blocks if you have uncommitted changes
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ For communication structure: `roboco_kb_search("communication hierarchy")`
|
|||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
```
|
```
|
||||||
SCAN → CLAIM → PLAN → CREATE_PARENT_BRANCH → CREATE GROUP → CREATE CELL TASKS → ACTIVATE → NOTIFY → PAUSE → MONITOR → REVIEW_PR → COMPLETE
|
SCAN → CLAIM → PLAN → CREATE GROUP → CREATE CELL TASKS → ACTIVATE → NOTIFY → PAUSE → MONITOR → REVIEW_PR → COMPLETE
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1. SCAN
|
### 1. SCAN
|
||||||
@@ -27,30 +27,10 @@ Use `roboco_task_scan()` for tasks assigned to you from Board/CEO.
|
|||||||
### 2. CLAIM + PLAN
|
### 2. CLAIM + PLAN
|
||||||
Claim → read full description → plan breakdown across cells → start → journal decision.
|
Claim → read full description → plan breakdown across cells → start → journal decision.
|
||||||
|
|
||||||
### 3. CREATE PARENT BRANCH (Git Tasks)
|
### 3. CREATE GROUP
|
||||||
**For tasks with `requires_git=True`:**
|
|
||||||
```
|
|
||||||
roboco_git_create_branch(project_slug, task_id, branch_type)
|
|
||||||
```
|
|
||||||
|
|
||||||
- Creates parent branch from the project's default branch (e.g., `main`, `master`)
|
|
||||||
- Cell PM subtask branches will fork from this
|
|
||||||
- Example: `feature/cross/abc123` for cross-cell work
|
|
||||||
|
|
||||||
**Branch Hierarchy for Git Tasks:**
|
|
||||||
- For hierarchical branching: default branch → Your branch → Cell PM branch → Dev branch
|
|
||||||
- If using hierarchical branches, create root branch before Cell PMs create theirs
|
|
||||||
- Cell PM branches fork from your branch (set as `parent_branch`)
|
|
||||||
|
|
||||||
**Typical order for hierarchical branching:**
|
|
||||||
1. Create your root branch from project's default branch
|
|
||||||
2. Create cell tasks, Cell PMs create branches from your branch
|
|
||||||
3. Devs create branches from Cell PM branches
|
|
||||||
|
|
||||||
### 4. CREATE GROUP
|
|
||||||
Use `roboco_group_create()` in each relevant cell channel. Cell PMs need groups to create sessions.
|
Use `roboco_group_create()` in each relevant cell channel. Cell PMs need groups to create sessions.
|
||||||
|
|
||||||
### 5. CREATE CELL TASKS
|
### 4. CREATE CELL TASKS
|
||||||
|
|
||||||
**CRITICAL: Always set `parent_task_id` to YOUR task ID.** Without this, you create orphan tasks, not subtasks.
|
**CRITICAL: Always set `parent_task_id` to YOUR task ID.** Without this, you create orphan tasks, not subtasks.
|
||||||
|
|
||||||
@@ -76,19 +56,18 @@ roboco_task_create(
|
|||||||
- Completion tracking breaks
|
- Completion tracking breaks
|
||||||
- Your task can't complete
|
- Your task can't complete
|
||||||
|
|
||||||
- Set `project_id` and `branch_name` for git tasks
|
- Set `project_id` for git tasks (branches are auto-created on claim)
|
||||||
- Cell PMs will create subtask branches from your parent branch
|
|
||||||
|
|
||||||
### 6. ACTIVATE + NOTIFY
|
### 5. ACTIVATE + NOTIFY
|
||||||
`roboco_task_activate()` each task, then `roboco_notify_send()` to each Cell PM. REQUIRED.
|
`roboco_task_activate()` each task, then `roboco_notify_send()` to each Cell PM. REQUIRED.
|
||||||
|
|
||||||
### 7. PAUSE + IDLE
|
### 6. PAUSE + IDLE
|
||||||
`roboco_task_pause()` with checkpoint, then `roboco_agent_idle()`.
|
`roboco_task_pause()` with checkpoint, then `roboco_agent_idle()`.
|
||||||
|
|
||||||
### 8. MONITOR
|
### 7. MONITOR
|
||||||
When respawned: scan, read Cell PM journals, update progress, coordinate if blockers.
|
When respawned: scan, read Cell PM journals, update progress, coordinate if blockers.
|
||||||
|
|
||||||
### 9. REVIEW PR (Git Tasks)
|
### 8. REVIEW PR (Git Tasks)
|
||||||
When cell tasks reach `awaiting_pm_review` and all subtasks are merged:
|
When cell tasks reach `awaiting_pm_review` and all subtasks are merged:
|
||||||
1. Review the parent PR (all subtask work combined)
|
1. Review the parent PR (all subtask work combined)
|
||||||
2. Coordinate with Cell PM - **BOTH must approve**
|
2. Coordinate with Cell PM - **BOTH must approve**
|
||||||
@@ -98,7 +77,7 @@ When cell tasks reach `awaiting_pm_review` and all subtasks are merged:
|
|||||||
|
|
||||||
**CEO merges the final PR to main.**
|
**CEO merges the final PR to main.**
|
||||||
|
|
||||||
### 10. COMPLETE
|
### 9. COMPLETE
|
||||||
When CEO approves and PR is merged: reflect + complete your task.
|
When CEO approves and PR is merged: reflect + complete your task.
|
||||||
|
|
||||||
## Your Tools
|
## Your Tools
|
||||||
@@ -121,10 +100,11 @@ When CEO approves and PR is merged: reflect + complete your task.
|
|||||||
- `roboco_git_diff(project_slug, staged)` - View changes
|
- `roboco_git_diff(project_slug, staged)` - View changes
|
||||||
|
|
||||||
**Git (PM Branch Management):**
|
**Git (PM Branch Management):**
|
||||||
- `roboco_git_create_branch(project_slug, task_id, branch_type, "main")` - Create parent branch
|
|
||||||
- `roboco_git_checkout(project_slug, branch)` - Switch branches
|
- `roboco_git_checkout(project_slug, branch)` - Switch branches
|
||||||
- `roboco_git_merge_pr(project_slug, pr_number, task_id, merge_method)` - Merge PR
|
- `roboco_git_merge_pr(project_slug, pr_number, task_id, merge_method)` - Merge PR
|
||||||
|
|
||||||
|
**Note:** Branches are auto-created when tasks are claimed. No manual branch creation needed.
|
||||||
|
|
||||||
**Group Management (Main PM ONLY):**
|
**Group Management (Main PM ONLY):**
|
||||||
- `roboco_group_create` - Create groups in channels
|
- `roboco_group_create` - Create groups in channels
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
3. Assign work to cell members
|
3. Assign work to cell members
|
||||||
4. Complete tasks after full workflow
|
4. Complete tasks after full workflow
|
||||||
5. Handle escalations from cell
|
5. Handle escalations from cell
|
||||||
6. Create branches for git tasks
|
6. Review and merge PRs for git tasks
|
||||||
|
|
||||||
## What You CAN Do
|
## What You CAN Do
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
- Unblock blocked tasks
|
- Unblock blocked tasks
|
||||||
- Send notifications
|
- Send notifications
|
||||||
- Index code and documentation
|
- Index code and documentation
|
||||||
- Create branches: `roboco_git_create_branch()`
|
- Merge PRs: `roboco_git_merge_pr()`
|
||||||
|
|
||||||
## What You CANNOT Do
|
## What You CANNOT Do
|
||||||
|
|
||||||
@@ -62,17 +62,13 @@ roboco_notify_send({
|
|||||||
|
|
||||||
For tasks with `requires_git=True`:
|
For tasks with `requires_git=True`:
|
||||||
|
|
||||||
```python
|
**Branches are auto-created when tasks are claimed:**
|
||||||
# Create branch BEFORE developer can start
|
- When you claim your task: `feature/team/MAIN_PM_ID/YOUR_ID`
|
||||||
roboco_git_create_branch(
|
- When devs claim their subtasks: `feature/team/MAIN_PM_ID/YOUR_ID/DEV_ID`
|
||||||
project_slug="roboco",
|
|
||||||
task_id=task_id,
|
|
||||||
branch_type="feature"
|
|
||||||
)
|
|
||||||
# Creates: feature/backend/a1b2c3d4
|
|
||||||
```
|
|
||||||
|
|
||||||
Developer cannot start (`claimed` → `in_progress`) until branch exists.
|
**No manual branch creation needed.** Just claim the task and the hierarchical branch is auto-created.
|
||||||
|
|
||||||
|
PRs merge bottom-up: dev branch → your branch → main PM branch → main.
|
||||||
|
|
||||||
## Completing Tasks
|
## Completing Tasks
|
||||||
|
|
||||||
@@ -120,7 +116,7 @@ See: `roboco_kb_search("tool permissions")`
|
|||||||
| `roboco_task_complete` | Finish task |
|
| `roboco_task_complete` | Finish task |
|
||||||
| `roboco_task_cancel` | Cancel task |
|
| `roboco_task_cancel` | Cancel task |
|
||||||
| `roboco_task_unblock` | Unblock blocked task |
|
| `roboco_task_unblock` | Unblock blocked task |
|
||||||
| `roboco_git_create_branch` | Create task branch |
|
| `roboco_git_merge_pr` | Merge developer PRs |
|
||||||
| `roboco_notify_send` | Send notification |
|
| `roboco_notify_send` | Send notification |
|
||||||
| `roboco_project_update` | Update own cell's projects |
|
| `roboco_project_update` | Update own cell's projects |
|
||||||
| `roboco_workspace_list` | List own cell's workspaces |
|
| `roboco_workspace_list` | List own cell's workspaces |
|
||||||
|
|||||||
@@ -46,11 +46,12 @@ pending → claim → plan → start → work → submit_verification → submit
|
|||||||
| State | Meaning |
|
| State | Meaning |
|
||||||
|-------|---------|
|
|-------|---------|
|
||||||
| `NEEDS_PLAN` | Must call `roboco_task_plan()` first |
|
| `NEEDS_PLAN` | Must call `roboco_task_plan()` first |
|
||||||
| `WAITING_FOR_BRANCH` | PM must create branch (git tasks) |
|
|
||||||
| `READY_TO_START` | Call `roboco_task_start()` |
|
| `READY_TO_START` | Call `roboco_task_start()` |
|
||||||
| `EXECUTING` | Work in progress |
|
| `EXECUTING` | Work in progress |
|
||||||
| `REVISION_REQUIRED` | Fix QA/PM feedback |
|
| `REVISION_REQUIRED` | Fix QA/PM feedback |
|
||||||
|
|
||||||
|
Note: Git branches are auto-created when you claim the task. No waiting needed.
|
||||||
|
|
||||||
## Tool Restrictions
|
## Tool Restrictions
|
||||||
|
|
||||||
Use `roboco_*` MCP tools, not native Claude tools:
|
Use `roboco_*` MCP tools, not native Claude tools:
|
||||||
|
|||||||
@@ -27,19 +27,18 @@ log = roboco_git_log(
|
|||||||
|
|
||||||
## Branch Operations
|
## Branch Operations
|
||||||
|
|
||||||
|
**Branches are auto-created when tasks are claimed:**
|
||||||
|
- Root task claim → `feature/team/ROOT_ID`
|
||||||
|
- Subtask claim → `feature/team/ROOT_ID/SUB_ID`
|
||||||
|
- Sub-subtask claim → `feature/team/ROOT_ID/SUB_ID/SUBSUB_ID`
|
||||||
|
|
||||||
|
No manual branch creation needed.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# List branches
|
# List branches
|
||||||
branches = roboco_git_branch_list(project_slug="roboco")
|
branches = roboco_git_branch_list(project_slug="roboco")
|
||||||
|
|
||||||
# Create branch (PM only)
|
# Checkout branch (if needed)
|
||||||
roboco_git_create_branch(
|
|
||||||
project_slug="roboco",
|
|
||||||
task_id=task_id,
|
|
||||||
branch_type="feature" # feature, bug, chore, docs, hotfix
|
|
||||||
)
|
|
||||||
# Creates: feature/backend/a1b2c3d4
|
|
||||||
|
|
||||||
# Checkout branch
|
|
||||||
roboco_git_checkout(
|
roboco_git_checkout(
|
||||||
project_slug="roboco",
|
project_slug="roboco",
|
||||||
branch="feature/backend/a1b2c3d4"
|
branch="feature/backend/a1b2c3d4"
|
||||||
|
|||||||
@@ -44,13 +44,14 @@
|
|||||||
|
|
||||||
See: `roboco_kb_search("task planning workflow")`
|
See: `roboco_kb_search("task planning workflow")`
|
||||||
|
|
||||||
## WAITING_FOR_BRANCH
|
## Parent Branch Required
|
||||||
|
|
||||||
**Symptom:** Can't start git task, state is WAITING_FOR_BRANCH
|
**Symptom:** Can't claim subtask, error "Parent task must be claimed first"
|
||||||
|
|
||||||
**Cause:** PM hasn't created the branch yet
|
**Cause:** Parent task hasn't been claimed yet, so it has no branch
|
||||||
|
|
||||||
**Solution:**
|
**Solution:**
|
||||||
1. Message PM to create branch
|
1. Parent task must be claimed first (branch auto-creates on claim)
|
||||||
2. Or escalate: `roboco_task_escalate(task_id, "Need branch")`
|
2. Then subtask can be claimed (its branch forks from parent's)
|
||||||
3. Wait for branch_name to be set on task
|
|
||||||
|
Note: Branches are auto-created hierarchically. No manual creation needed.
|
||||||
|
|||||||
@@ -27,14 +27,14 @@
|
|||||||
|
|
||||||
**Causes**:
|
**Causes**:
|
||||||
1. Task not claimed by you
|
1. Task not claimed by you
|
||||||
2. For git tasks: branch not created yet
|
2. Task in wrong status
|
||||||
3. Task in wrong status
|
|
||||||
|
|
||||||
**Solutions**:
|
**Solutions**:
|
||||||
- Claim first: `roboco_task_claim(task_id)`
|
- Claim first: `roboco_task_claim(task_id)`
|
||||||
- Wait for PM to create branch (git tasks)
|
|
||||||
- Check current status
|
- Check current status
|
||||||
|
|
||||||
|
Note: Git branches are auto-created on claim, no waiting needed.
|
||||||
|
|
||||||
## Cannot Submit for QA
|
## Cannot Submit for QA
|
||||||
|
|
||||||
**Error**: "Invalid transition from current status"
|
**Error**: "Invalid transition from current status"
|
||||||
@@ -74,16 +74,15 @@ parent_id = task.parent_task_id
|
|||||||
roboco_task_escalate_to_ceo(parent_id, notes="...")
|
roboco_task_escalate_to_ceo(parent_id, notes="...")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Git Task: No Branch
|
## Git Task: Parent Branch Required
|
||||||
|
|
||||||
**Error**: "Branch name required for git tasks"
|
**Error**: "Parent task must be claimed first to create its branch"
|
||||||
|
|
||||||
**Cause**: PM hasn't created branch yet
|
**Cause**: Trying to claim a subtask when parent task hasn't been claimed yet
|
||||||
|
|
||||||
**Solution**: Wait for PM or ask PM to create branch:
|
**Solution**: Parent task must be claimed first. Branches are auto-created hierarchically:
|
||||||
```python
|
1. Parent is claimed → parent branch created
|
||||||
roboco_git_create_branch(project_slug, task_id, "feature")
|
2. Then subtask can be claimed → subtask branch created (forked from parent)
|
||||||
```
|
|
||||||
|
|
||||||
## Task Has Incomplete Subtasks
|
## Task Has Incomplete Subtasks
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ Branches follow: `{type}/{team}/{root-uuid}[/{subtask-uuid}[/{sub-sub-uuid}]]`
|
|||||||
- Subtask: `feature/backend/550e8400.../6ba7b810...`
|
- Subtask: `feature/backend/550e8400.../6ba7b810...`
|
||||||
- Sub-sub: `feature/backend/550e8400.../6ba7b810.../f47ac10b...`
|
- Sub-sub: `feature/backend/550e8400.../6ba7b810.../f47ac10b...`
|
||||||
|
|
||||||
PM creates branches via `roboco_git_create_branch()`.
|
**Branches are auto-created when tasks are claimed.** No manual creation needed.
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ CLAIM → PLAN → START → EXECUTE
|
|||||||
| State | Meaning | Next Step |
|
| State | Meaning | Next Step |
|
||||||
|-------|---------|-----------|
|
|-------|---------|-----------|
|
||||||
| `NEEDS_PLAN` | Task claimed, no plan yet | Call `roboco_task_plan()` |
|
| `NEEDS_PLAN` | Task claimed, no plan yet | Call `roboco_task_plan()` |
|
||||||
| `WAITING_FOR_BRANCH` | Plan approved, git task needs branch | PM creates branch |
|
|
||||||
| `READY_TO_START` | Plan approved, ready to work | Call `roboco_task_start()` |
|
| `READY_TO_START` | Plan approved, ready to work | Call `roboco_task_start()` |
|
||||||
| `EXECUTING` | Work in progress | Continue development |
|
| `EXECUTING` | Work in progress | Continue development |
|
||||||
| `REVISION_REQUIRED` | QA/PM requested changes | Reclaim and fix |
|
| `REVISION_REQUIRED` | QA/PM requested changes | Reclaim and fix |
|
||||||
@@ -38,12 +37,12 @@ Calling `roboco_task_start()` without a plan returns:
|
|||||||
- Message: "Cannot start without a plan"
|
- Message: "Cannot start without a plan"
|
||||||
- Hint: Submit plan first
|
- Hint: Submit plan first
|
||||||
|
|
||||||
## Git Tasks Need Branch
|
## Git Tasks
|
||||||
|
|
||||||
For tasks with `requires_git=True`:
|
For tasks with `requires_git=True`:
|
||||||
1. Submit plan
|
- **Branches are auto-created when you claim the task**
|
||||||
2. PM creates branch: `roboco_git_create_branch()`
|
- Root tasks: branch created from default branch (main/master)
|
||||||
3. Task gets `branch_name` field set
|
- Subtasks: branch forked from parent's branch
|
||||||
4. Then you can call `roboco_task_start()`
|
- No manual `roboco_git_create_branch()` needed
|
||||||
|
|
||||||
If no branch: workflow state = `WAITING_FOR_BRANCH`
|
Hierarchical branch naming: `feature/team/ROOT_ID/SUB_ID/SUBSUB_ID`
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ from roboco.api.schemas.git import (
|
|||||||
GitStatusResponse,
|
GitStatusResponse,
|
||||||
)
|
)
|
||||||
from roboco.exceptions import GitCommandError, GitTimeoutError
|
from roboco.exceptions import GitCommandError, GitTimeoutError
|
||||||
|
from roboco.models.base import TaskStatus
|
||||||
from roboco.services.base import NotFoundError, ServiceError, ValidationError
|
from roboco.services.base import NotFoundError, ServiceError, ValidationError
|
||||||
from roboco.services.git import get_git_service
|
from roboco.services.git import get_git_service
|
||||||
|
from roboco.services.project import get_project_service
|
||||||
from roboco.services.task import get_task_service
|
from roboco.services.task import get_task_service
|
||||||
from roboco.services.work_session import get_work_session_service
|
from roboco.services.work_session import get_work_session_service
|
||||||
from roboco.utils.converters import require_uuid
|
from roboco.utils.converters import require_uuid
|
||||||
@@ -356,17 +358,55 @@ async def create_branch(
|
|||||||
"""
|
"""
|
||||||
git_service = get_git_service(db)
|
git_service = get_git_service(db)
|
||||||
task_service = get_task_service(db)
|
task_service = get_task_service(db)
|
||||||
|
project_service = get_project_service(db)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
workspace = await git_service.get_workspace(data.project_slug, agent.agent_id)
|
workspace = await git_service.get_workspace(data.project_slug, agent.agent_id)
|
||||||
|
|
||||||
|
task_uuid = UUID(data.task_id)
|
||||||
|
task = await task_service.get(task_uuid)
|
||||||
|
project = await project_service.get_by_slug(data.project_slug)
|
||||||
|
|
||||||
|
project_cell: str | None = None
|
||||||
|
if project and project.assigned_cell:
|
||||||
|
project_cell = (
|
||||||
|
project.assigned_cell.value
|
||||||
|
if hasattr(project.assigned_cell, "value")
|
||||||
|
else str(project.assigned_cell)
|
||||||
|
)
|
||||||
|
|
||||||
|
task_team: str | None = None
|
||||||
|
if task and task.team:
|
||||||
|
task_team = (
|
||||||
|
task.team.value if hasattr(task.team, "value") else str(task.team)
|
||||||
|
)
|
||||||
|
|
||||||
|
if project_cell == "fullstack":
|
||||||
|
effective_team = task_team or "cross"
|
||||||
|
team_for_branch = f"{data.project_slug}/{effective_team}"
|
||||||
|
elif project_cell:
|
||||||
|
team_for_branch = project_cell
|
||||||
|
elif task_team:
|
||||||
|
team_for_branch = task_team
|
||||||
|
else:
|
||||||
|
team_for_branch = "cross"
|
||||||
|
|
||||||
branch_name, created_from = await git_service.create_branch(
|
branch_name, created_from = await git_service.create_branch(
|
||||||
workspace, agent.team or "unknown", data
|
workspace, team_for_branch, data
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store branch name on task
|
|
||||||
task_uuid = UUID(data.task_id)
|
|
||||||
await task_service.update(task_uuid, branch_name=branch_name)
|
await task_service.update(task_uuid, branch_name=branch_name)
|
||||||
|
|
||||||
|
children = await task_service.get_subtasks(task_uuid)
|
||||||
|
for child in children:
|
||||||
|
if (
|
||||||
|
child.status == TaskStatus.BACKLOG
|
||||||
|
and child.requires_git
|
||||||
|
and not child.branch_name
|
||||||
|
):
|
||||||
|
child_uuid = UUID(str(child.id))
|
||||||
|
await task_service.update(child_uuid, branch_name=branch_name)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
except ServiceError as e:
|
except ServiceError as e:
|
||||||
raise _translate_error(e) from e
|
raise _translate_error(e) from e
|
||||||
|
|||||||
@@ -431,6 +431,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,
|
||||||
|
group_id=data.group_id,
|
||||||
scope=data.scope,
|
scope=data.scope,
|
||||||
relationship_type=rel_type,
|
relationship_type=rel_type,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1638,11 +1638,28 @@ async def escalate_task(
|
|||||||
delivery_service = get_notification_delivery_service(db)
|
delivery_service = get_notification_delivery_service(db)
|
||||||
await delivery_service.deliver(require_uuid(notification.id))
|
await delivery_service.deliver(require_uuid(notification.id))
|
||||||
|
|
||||||
|
# CRITICAL FIX: Set task to BLOCKED to stop orchestrator from respawning dev
|
||||||
|
# Previously used PENDING which could still cause respawn loops.
|
||||||
|
# BLOCKED ensures task is truly paused until PM unblocks it.
|
||||||
|
task.assigned_to = target_agent.id
|
||||||
|
task.status = TaskStatus.BLOCKED # Blocked until PM addresses it
|
||||||
|
|
||||||
|
# Add escalation note for context
|
||||||
|
existing_notes = task.dev_notes or ""
|
||||||
|
escalation_note = (
|
||||||
|
f"\n\n[ESCALATED] From {agent_record.slug} to {target_slug}\n"
|
||||||
|
f"Reason: {data.reason}"
|
||||||
|
)
|
||||||
|
task.dev_notes = existing_notes + escalation_note
|
||||||
|
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
msg = (
|
msg = (
|
||||||
f"Task escalated to {target_slug}. "
|
f"Task escalated to {target_slug} and set to BLOCKED. "
|
||||||
"They will be notified and can reassign or provide guidance."
|
f"PM will receive notification and must call roboco_task_unblock() "
|
||||||
|
"to provide guidance or reassign."
|
||||||
)
|
)
|
||||||
return EscalateResponse(
|
return EscalateResponse(
|
||||||
status="escalated",
|
status="escalated",
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ class SessionForTasksCreateRequest(BaseModel):
|
|||||||
|
|
||||||
task_ids: list[UUID] = Field(..., min_length=1)
|
task_ids: list[UUID] = Field(..., min_length=1)
|
||||||
channel_slug: str
|
channel_slug: str
|
||||||
|
group_id: UUID | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Optional group ID to place session under",
|
||||||
|
)
|
||||||
scope: SessionScope = Field(
|
scope: SessionScope = Field(
|
||||||
default=SessionScope.CELL,
|
default=SessionScope.CELL,
|
||||||
description="Scope level: initiative (Main PM), cell (Cell PM), task (dev)",
|
description="Scope level: initiative (Main PM), cell (Cell PM), task (dev)",
|
||||||
|
|||||||
+1
-1
@@ -143,7 +143,7 @@ class TaskTable(Base):
|
|||||||
)
|
)
|
||||||
requires_git: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
requires_git: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||||
|
|
||||||
# Project & Branch (set by PM during setup)
|
# Project & Branch (branch auto-created on claim)
|
||||||
project_id: Mapped[UUID | None] = mapped_column(
|
project_id: Mapped[UUID | None] = mapped_column(
|
||||||
UUID(as_uuid=True),
|
UUID(as_uuid=True),
|
||||||
ForeignKey("projects.id", ondelete="SET NULL"),
|
ForeignKey("projects.id", ondelete="SET NULL"),
|
||||||
|
|||||||
@@ -40,12 +40,15 @@ from roboco.enforcement.notification_perms import (
|
|||||||
from roboco.enforcement.task_lifecycle import (
|
from roboco.enforcement.task_lifecycle import (
|
||||||
ROLE_RESTRICTED_TRANSITIONS,
|
ROLE_RESTRICTED_TRANSITIONS,
|
||||||
VALID_TRANSITIONS,
|
VALID_TRANSITIONS,
|
||||||
|
GitContext,
|
||||||
|
GitRequirementError,
|
||||||
TaskLifecycleError,
|
TaskLifecycleError,
|
||||||
can_agent_transition,
|
can_agent_transition,
|
||||||
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_git_requirements,
|
||||||
validate_task_transition,
|
validate_task_transition,
|
||||||
)
|
)
|
||||||
from roboco.enforcement.task_ownership import (
|
from roboco.enforcement.task_ownership import (
|
||||||
@@ -61,6 +64,8 @@ __all__ = [
|
|||||||
"ROLE_RESTRICTED_TRANSITIONS",
|
"ROLE_RESTRICTED_TRANSITIONS",
|
||||||
"VALID_TRANSITIONS",
|
"VALID_TRANSITIONS",
|
||||||
"ChannelAccessDeniedError",
|
"ChannelAccessDeniedError",
|
||||||
|
"GitContext",
|
||||||
|
"GitRequirementError",
|
||||||
"JournalAccessDeniedError",
|
"JournalAccessDeniedError",
|
||||||
"NotificationPermissionError",
|
"NotificationPermissionError",
|
||||||
"TaskClaimContext",
|
"TaskClaimContext",
|
||||||
@@ -77,6 +82,7 @@ __all__ = [
|
|||||||
"is_terminal_state",
|
"is_terminal_state",
|
||||||
"is_waiting_state",
|
"is_waiting_state",
|
||||||
"validate_channel_access",
|
"validate_channel_access",
|
||||||
|
"validate_git_requirements",
|
||||||
"validate_journal_access",
|
"validate_journal_access",
|
||||||
"validate_notification_permission",
|
"validate_notification_permission",
|
||||||
"validate_task_claim",
|
"validate_task_claim",
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ def validate_git_requirements(
|
|||||||
Requires pr_number to be set (PR exists)
|
Requires pr_number to be set (PR exists)
|
||||||
|
|
||||||
- claimed → in_progress (git tasks):
|
- claimed → in_progress (git tasks):
|
||||||
Should have branch_name set (PM created the branch)
|
Should have branch_name set (auto-created on claim)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
current_status: Current task status
|
current_status: Current task status
|
||||||
@@ -361,8 +361,8 @@ def validate_git_requirements(
|
|||||||
requirement="branch_name",
|
requirement="branch_name",
|
||||||
message=(
|
message=(
|
||||||
"Cannot start work: no branch assigned to this task. "
|
"Cannot start work: no branch assigned to this task. "
|
||||||
"For git tasks, PM must create a branch first using "
|
"Branches are auto-created on claim. If missing, either "
|
||||||
"roboco_git_create_branch(project_slug, task_id, branch_type)."
|
"re-claim the task or check if parent task needs claiming first."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ def create_git_mcp_server(agent_id: str) -> FastMCP:
|
|||||||
Tools are registered based on role:
|
Tools are registered based on role:
|
||||||
- All agents: read-only tools (status, log, branch list, diff)
|
- All agents: read-only tools (status, log, branch list, diff)
|
||||||
- Developers: commit, push, create PR
|
- Developers: commit, push, create PR
|
||||||
- PMs: create branch, checkout, merge PR
|
- PMs: checkout, merge PR (branches auto-created on claim)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
agent_id: The agent identifier (e.g., "be-dev-1")
|
agent_id: The agent identifier (e.g., "be-dev-1")
|
||||||
|
|||||||
@@ -174,8 +174,10 @@ class TaskCreateInput(BaseModel):
|
|||||||
- Use roboco_project_list() to see available projects
|
- Use roboco_project_list() to see available projects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
title: str = Field(..., description="Task title")
|
title: str = Field(..., min_length=1, max_length=200, description="Task title")
|
||||||
description: str = Field(..., description="Task description")
|
description: str = Field(
|
||||||
|
..., min_length=10, description="Task description (min 10 chars)"
|
||||||
|
)
|
||||||
acceptance_criteria: list[str] = Field(
|
acceptance_criteria: list[str] = Field(
|
||||||
..., min_length=1, description="At least one acceptance criterion"
|
..., min_length=1, description="At least one acceptance criterion"
|
||||||
)
|
)
|
||||||
@@ -192,6 +194,13 @@ class TaskCreateInput(BaseModel):
|
|||||||
default=True,
|
default=True,
|
||||||
description="Whether task requires git. If True, project_slug is required.",
|
description="Whether task requires git. If True, project_slug is required.",
|
||||||
)
|
)
|
||||||
|
task_type: str = Field(
|
||||||
|
default="code",
|
||||||
|
description=(
|
||||||
|
"Task type: code (git work), documentation, research, planning, "
|
||||||
|
"design, administrative. For subtasks, inherits from parent if not set."
|
||||||
|
),
|
||||||
|
)
|
||||||
parent_task_id: str | None = Field(
|
parent_task_id: str | None = Field(
|
||||||
default=None, description="Parent task for subtasks"
|
default=None, description="Parent task for subtasks"
|
||||||
)
|
)
|
||||||
@@ -269,6 +278,10 @@ class SessionCreateForTasksInput(BaseModel):
|
|||||||
..., min_length=1, description="Task IDs to link to the session"
|
..., min_length=1, description="Task IDs to link to the session"
|
||||||
)
|
)
|
||||||
channel_slug: str = Field(..., description="Channel where session is created")
|
channel_slug: str = Field(..., description="Channel where session is created")
|
||||||
|
group_id: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Group ID to place session under (from roboco_group_create)",
|
||||||
|
)
|
||||||
scope: str = Field(
|
scope: str = Field(
|
||||||
default="cell",
|
default="cell",
|
||||||
description="Scope level: initiative (Main PM), cell (Cell PM), task (dev)",
|
description="Scope level: initiative (Main PM), cell (Cell PM), task (dev)",
|
||||||
|
|||||||
@@ -139,6 +139,20 @@ async def validate_task_claimable(
|
|||||||
Special case: If an agent is already assigned to a pending task (PM assigned
|
Special case: If an agent is already assigned to a pending task (PM assigned
|
||||||
it directly to them), they can claim it to transition to 'claimed' status.
|
it directly to them), they can claim it to transition to 'claimed' status.
|
||||||
"""
|
"""
|
||||||
|
# ENFORCEMENT: Main PM must delegate code tasks, not execute them
|
||||||
|
task_type = task.get("task_type", "code")
|
||||||
|
if agent_role == "main_pm" and task_type == "code":
|
||||||
|
return format_error_response(
|
||||||
|
"PM_CANNOT_EXECUTE_CODE",
|
||||||
|
"Main PM cannot claim code tasks. You coordinate, not execute.",
|
||||||
|
{"task_type": task_type, "your_role": agent_role},
|
||||||
|
hint=(
|
||||||
|
"Create a subtask for the appropriate Cell PM (be-pm, fe-pm, ux-pm) "
|
||||||
|
"using roboco_task_create(parent_task_id=this_task_id, team='backend', "
|
||||||
|
"assigned_to='be-pm'). Then activate it with roboco_task_activate()."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
task_status = task.get("status")
|
task_status = task.get("status")
|
||||||
claimable_statuses = {
|
claimable_statuses = {
|
||||||
# QA: pending (direct QA tasks from PM) or awaiting_qa (normal workflow)
|
# QA: pending (direct QA tasks from PM) or awaiting_qa (normal workflow)
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ Handler for claiming tasks.
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from starlette import status
|
|
||||||
|
|
||||||
from roboco.agents_config import get_agent_role
|
from roboco.agents_config import get_agent_role
|
||||||
from roboco.mcp.tasks import format_task_response
|
from roboco.mcp.tasks import format_task_response
|
||||||
from roboco.mcp.tasks.handlers._helpers import (
|
from roboco.mcp.tasks.handlers._helpers import (
|
||||||
@@ -58,51 +56,6 @@ async def _is_pre_assigned_to_agent(
|
|||||||
return agent_uuid is not None and str(assigned_to) == agent_uuid
|
return agent_uuid is not None and str(assigned_to) == agent_uuid
|
||||||
|
|
||||||
|
|
||||||
async def _validate_branch_hierarchy(
|
|
||||||
client: ApiClient, task: dict[str, Any]
|
|
||||||
) -> dict[str, Any] | None:
|
|
||||||
"""Walk up task hierarchy ensuring all git-enabled ancestors have branches.
|
|
||||||
|
|
||||||
Returns error dict if hierarchy is incomplete, None if valid.
|
|
||||||
Branch hierarchy must be created from root down:
|
|
||||||
- Main PM creates root branch
|
|
||||||
- Cell PM creates subtask branch
|
|
||||||
- Developer works on subsubtask branch
|
|
||||||
"""
|
|
||||||
current_parent_id = task.get("parent_task_id")
|
|
||||||
ancestors_checked: list[str] = []
|
|
||||||
|
|
||||||
while current_parent_id:
|
|
||||||
parent_resp = await client.get(f"/tasks/{current_parent_id}")
|
|
||||||
if parent_resp.status_code != status.HTTP_200_OK:
|
|
||||||
# Can't fetch parent - may have been deleted, allow to proceed
|
|
||||||
break
|
|
||||||
|
|
||||||
parent = parent_resp.json()
|
|
||||||
parent_title = parent.get("title", str(current_parent_id)[:8])
|
|
||||||
ancestors_checked.append(parent_title)
|
|
||||||
|
|
||||||
# Check if parent is a git task missing its branch
|
|
||||||
if parent.get("requires_git") and not parent.get("branch_name"):
|
|
||||||
return format_error_response(
|
|
||||||
"BRANCH_HIERARCHY_INCOMPLETE",
|
|
||||||
f"Parent task '{parent_title}' has no branch yet.",
|
|
||||||
{
|
|
||||||
"missing_branch_task": str(current_parent_id),
|
|
||||||
"ancestors_checked": ancestors_checked,
|
|
||||||
},
|
|
||||||
hint=(
|
|
||||||
"Branch hierarchy must be created from root down. "
|
|
||||||
"Main PM creates root branch, Cell PM creates subtask branch. "
|
|
||||||
"Ask your PM to create the parent branch first."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
current_parent_id = parent.get("parent_task_id")
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def _execute_claim(
|
async def _execute_claim(
|
||||||
client: ApiClient, task_id: str, agent_id: str
|
client: ApiClient, task_id: str, agent_id: str
|
||||||
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
||||||
@@ -122,45 +75,105 @@ async def _execute_claim(
|
|||||||
async def _validate_git_requirements(
|
async def _validate_git_requirements(
|
||||||
client: ApiClient, task: dict[str, Any], task_id: str
|
client: ApiClient, task: dict[str, Any], task_id: str
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""Validate git-related requirements. Returns error or None."""
|
"""Validate git requirements for hierarchical branching.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Must have project_id (always)
|
||||||
|
- Root tasks: branch auto-created from default branch
|
||||||
|
- Subtasks: need parent to have branch (for forking)
|
||||||
|
"""
|
||||||
if not task.get("requires_git"):
|
if not task.get("requires_git"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Branch must exist for git tasks
|
# Must have project
|
||||||
if not task.get("branch_name"):
|
if not task.get("project_id"):
|
||||||
return format_error_response(
|
return format_error_response(
|
||||||
"BRANCH_REQUIRED",
|
"PROJECT_REQUIRED",
|
||||||
"Cannot claim git task - PM must create branch first.",
|
"Git tasks require a project for branch creation.",
|
||||||
{"task_id": task_id, "requires_git": True},
|
{"task_id": task_id},
|
||||||
hint="PM should call roboco_git_create_branch() before developer can claim",
|
hint="Assign a project to this task.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate full branch hierarchy for git tasks with parent
|
# If already has branch, good to go
|
||||||
if task.get("parent_task_id"):
|
if task.get("branch_name"):
|
||||||
return await _validate_branch_hierarchy(client, task)
|
return None
|
||||||
|
|
||||||
|
# For subtasks, parent must have branch (so we can fork from it)
|
||||||
|
parent_id = task.get("parent_task_id")
|
||||||
|
if parent_id:
|
||||||
|
parent_resp = await client.get(f"/tasks/{parent_id}")
|
||||||
|
if parent_resp.ok:
|
||||||
|
parent = parent_resp.json()
|
||||||
|
if parent.get("requires_git") and not parent.get("branch_name"):
|
||||||
|
return format_error_response(
|
||||||
|
"PARENT_BRANCH_REQUIRED",
|
||||||
|
"Parent task must be claimed first to create its branch.",
|
||||||
|
{
|
||||||
|
"task_id": task_id,
|
||||||
|
"parent_id": parent_id,
|
||||||
|
"parent_title": parent.get("title", "")[:50],
|
||||||
|
},
|
||||||
|
hint="Parent task must be claimed first. Branch is auto-created.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Root task or parent has branch - will auto-create on claim
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _validate_sibling_sequence(
|
||||||
|
client: ApiClient, task: dict[str, Any]
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Ensure earlier sequence siblings are complete before claiming.
|
||||||
|
|
||||||
|
If task has sequence=2, then sequence=1 siblings must be completed first.
|
||||||
|
"""
|
||||||
|
parent_id = task.get("parent_task_id")
|
||||||
|
if not parent_id:
|
||||||
|
return None # Root task - no siblings
|
||||||
|
|
||||||
|
my_sequence = task.get("sequence", 0)
|
||||||
|
if my_sequence == 0:
|
||||||
|
return None # First in sequence, no prior siblings
|
||||||
|
|
||||||
|
# Get all sibling tasks (same parent)
|
||||||
|
siblings_resp = await client.get(f"/tasks?parent_task_id={parent_id}")
|
||||||
|
if not siblings_resp.ok:
|
||||||
|
return None
|
||||||
|
|
||||||
|
siblings = siblings_resp.json()
|
||||||
|
|
||||||
|
# Check if any earlier sequence siblings are NOT complete
|
||||||
|
terminal_statuses = ["completed", "cancelled"]
|
||||||
|
for sibling in siblings:
|
||||||
|
sibling_seq = sibling.get("sequence", 0)
|
||||||
|
sibling_status = sibling.get("status")
|
||||||
|
|
||||||
|
# Skip self
|
||||||
|
if sibling.get("id") == task.get("id"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If sibling has lower sequence and isn't complete, block
|
||||||
|
if sibling_seq < my_sequence and sibling_status not in terminal_statuses:
|
||||||
|
return format_error_response(
|
||||||
|
"SEQUENCE_ORDER_VIOLATION",
|
||||||
|
f"Cannot claim - task with sequence {sibling_seq} must complete first.",
|
||||||
|
{
|
||||||
|
"your_sequence": my_sequence,
|
||||||
|
"blocking_task_id": sibling.get("id"),
|
||||||
|
"blocking_sequence": sibling_seq,
|
||||||
|
"blocking_status": sibling_status,
|
||||||
|
"blocking_title": sibling.get("title", "")[:50],
|
||||||
|
},
|
||||||
|
hint=f"Wait for '{sibling.get('title', '')[:30]}' to complete.",
|
||||||
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def handle_task_claim(
|
async def _run_claim_validations(
|
||||||
client: ApiClient, task_id: str, agent_id: str
|
client: ApiClient, task: dict[str, Any], task_id: str, agent_id: str
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any] | None:
|
||||||
"""Handle task claiming.
|
"""Run all claim validations. Returns error dict or None if all pass."""
|
||||||
|
|
||||||
Flow:
|
|
||||||
1. Fetch the task first
|
|
||||||
2. Check if it's pre-assigned to this agent (PM assigned directly)
|
|
||||||
3. If pre-assigned, skip blocking check for THIS task
|
|
||||||
4. Otherwise, run full blocking check
|
|
||||||
5. Validate task is claimable for this role
|
|
||||||
6. Execute claim
|
|
||||||
"""
|
|
||||||
# Fetch task first - we need to check if it's pre-assigned
|
|
||||||
task, error = await fetch_task_or_error(client, task_id)
|
|
||||||
if error:
|
|
||||||
return error
|
|
||||||
assert task is not None
|
|
||||||
|
|
||||||
# Check if this task is pre-assigned to the agent
|
# Check if this task is pre-assigned to the agent
|
||||||
is_pre_assigned = await _is_pre_assigned_to_agent(task, agent_id, client)
|
is_pre_assigned = await _is_pre_assigned_to_agent(task, agent_id, client)
|
||||||
|
|
||||||
@@ -174,10 +187,38 @@ async def handle_task_claim(
|
|||||||
if error := await validate_task_claimable(task, agent_role, agent_id, client):
|
if error := await validate_task_claimable(task, agent_role, agent_id, client):
|
||||||
return error
|
return error
|
||||||
|
|
||||||
# Validate git requirements (branch exists, hierarchy valid)
|
# Validate git requirements (project exists, parent has branch if subtask)
|
||||||
if error := await _validate_git_requirements(client, task, task_id):
|
if error := await _validate_git_requirements(client, task, task_id):
|
||||||
return error
|
return error
|
||||||
|
|
||||||
|
# TODO: Re-enable when sequence workflow is refined
|
||||||
|
# Validate sibling sequence order (earlier sequence must complete first)
|
||||||
|
# if error := await _validate_sibling_sequence(client, task):
|
||||||
|
# return error
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_task_claim(
|
||||||
|
client: ApiClient, task_id: str, agent_id: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Handle task claiming.
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Fetch the task first
|
||||||
|
2. Run validations (blocking tasks, role, git, sequence)
|
||||||
|
3. Execute claim
|
||||||
|
"""
|
||||||
|
# Fetch task first
|
||||||
|
task, error = await fetch_task_or_error(client, task_id)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
assert task is not None
|
||||||
|
|
||||||
|
# Run all validations
|
||||||
|
if error := await _run_claim_validations(client, task, task_id, agent_id):
|
||||||
|
return error
|
||||||
|
|
||||||
# Execute the claim
|
# Execute the claim
|
||||||
claimed_task, error = await _execute_claim(client, task_id, agent_id)
|
claimed_task, error = await _execute_claim(client, task_id, agent_id)
|
||||||
if error:
|
if error:
|
||||||
|
|||||||
@@ -311,9 +311,29 @@ async def _fetch_and_validate_project(
|
|||||||
|
|
||||||
|
|
||||||
def _build_task_payload(
|
def _build_task_payload(
|
||||||
input_data: TaskCreateInput, project_id: str | None = None
|
input_data: TaskCreateInput,
|
||||||
|
project_id: str | None = None,
|
||||||
|
parent_task: dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Build task creation payload from input data."""
|
"""Build task creation payload from input data.
|
||||||
|
|
||||||
|
For subtasks, inherits task_type from parent if not explicitly set to non-default.
|
||||||
|
If assigning to a PM, task_type defaults to 'planning' (PMs coordinate, not code).
|
||||||
|
"""
|
||||||
|
# Determine task_type
|
||||||
|
task_type = input_data.task_type
|
||||||
|
|
||||||
|
# If assigning to a PM, default to 'planning' (PMs don't code)
|
||||||
|
if input_data.assigned_to and task_type == "code":
|
||||||
|
assignee_role = get_agent_role(input_data.assigned_to)
|
||||||
|
if assignee_role in ("cell_pm", "main_pm"):
|
||||||
|
task_type = "planning"
|
||||||
|
|
||||||
|
# For subtasks, inherit from parent if still at default
|
||||||
|
if parent_task and task_type == "code":
|
||||||
|
parent_type = parent_task.get("task_type", "code")
|
||||||
|
task_type = parent_type
|
||||||
|
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"title": input_data.title,
|
"title": input_data.title,
|
||||||
"description": input_data.description,
|
"description": input_data.description,
|
||||||
@@ -325,9 +345,12 @@ def _build_task_payload(
|
|||||||
"status": input_data.status, # Always included, defaults to "backlog"
|
"status": input_data.status, # Always included, defaults to "backlog"
|
||||||
"sequence": input_data.sequence, # Task ordering (lower = first)
|
"sequence": input_data.sequence, # Task ordering (lower = first)
|
||||||
"requires_git": input_data.requires_git,
|
"requires_git": input_data.requires_git,
|
||||||
|
"task_type": task_type,
|
||||||
}
|
}
|
||||||
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
|
||||||
|
# NOTE: Branch is auto-created on claim, not inherited at creation time.
|
||||||
|
# Each task in the hierarchy gets its own branch forked from parent's branch.
|
||||||
if input_data.dependency_ids:
|
if input_data.dependency_ids:
|
||||||
payload["dependency_ids"] = input_data.dependency_ids
|
payload["dependency_ids"] = input_data.dependency_ids
|
||||||
if project_id:
|
if project_id:
|
||||||
@@ -365,6 +388,40 @@ async def _validate_task_create_inputs(
|
|||||||
if error := _validate_cell_pm_team(agent_id, input_data.team):
|
if error := _validate_cell_pm_team(agent_id, input_data.team):
|
||||||
return None, error
|
return None, error
|
||||||
|
|
||||||
|
# ENFORCEMENT: All tasks require meaningful descriptions
|
||||||
|
# Main PM delegating to Cell PM needs to explain the work clearly
|
||||||
|
description = input_data.description.strip()
|
||||||
|
min_root_description_len = 30
|
||||||
|
min_subtask_description_len = 50
|
||||||
|
|
||||||
|
is_subtask = input_data.parent_task_id is not None
|
||||||
|
min_len = min_subtask_description_len if is_subtask else min_root_description_len
|
||||||
|
|
||||||
|
if is_subtask and len(description) < min_len:
|
||||||
|
# Subtasks need more detail (Cell PM → Dev)
|
||||||
|
return None, format_error_response(
|
||||||
|
"SUBTASK_DESCRIPTION_REQUIRED",
|
||||||
|
f"Subtasks MUST have detailed descriptions "
|
||||||
|
f"(min {min_len} chars, got {len(description)}). "
|
||||||
|
"Explain what to do, why, and expected outcome.",
|
||||||
|
{
|
||||||
|
"parent_task_id": input_data.parent_task_id,
|
||||||
|
"description_length": len(description),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
elif not is_subtask and len(description) < min_len:
|
||||||
|
# Root tasks also need context (Main PM → Cell PM)
|
||||||
|
return None, format_error_response(
|
||||||
|
"TASK_DESCRIPTION_REQUIRED",
|
||||||
|
f"Tasks MUST have meaningful descriptions "
|
||||||
|
f"(min {min_len} chars, got {len(description)}). "
|
||||||
|
"Main PM must explain what Cell PM should accomplish.",
|
||||||
|
{
|
||||||
|
"description_length": len(description),
|
||||||
|
"guidance": "Include: goal, context, and acceptance criteria.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Validate assignee BEFORE creating task
|
# Validate assignee BEFORE creating task
|
||||||
assignee = input_data.assigned_to
|
assignee = input_data.assigned_to
|
||||||
if assignee:
|
if assignee:
|
||||||
@@ -384,7 +441,41 @@ async def handle_task_create(
|
|||||||
if error:
|
if error:
|
||||||
return error
|
return error
|
||||||
|
|
||||||
payload = _build_task_payload(input_data, project_id)
|
# Fetch parent task for inheritance (task_type, etc.)
|
||||||
|
parent_task = None
|
||||||
|
if input_data.parent_task_id:
|
||||||
|
parent_resp = await client.get(f"/tasks/{input_data.parent_task_id}")
|
||||||
|
if parent_resp.ok:
|
||||||
|
parent_task = parent_resp.json()
|
||||||
|
|
||||||
|
# GUARDRAIL: If parent requires git, child must also require git (hierarchy)
|
||||||
|
if parent_task and parent_task.get("requires_git") and not input_data.requires_git:
|
||||||
|
return format_error_response(
|
||||||
|
"GIT_INHERITANCE_REQUIRED",
|
||||||
|
"Subtasks of git tasks must also require git.",
|
||||||
|
{
|
||||||
|
"parent_task_id": parent_task["id"],
|
||||||
|
"parent_requires_git": True,
|
||||||
|
"child_requires_git": input_data.requires_git,
|
||||||
|
},
|
||||||
|
hint="Set requires_git=True (or omit it, defaults to True).",
|
||||||
|
)
|
||||||
|
|
||||||
|
# GUARDRAIL: Git subtasks need parent to have branch (for forking)
|
||||||
|
if parent_task and input_data.requires_git:
|
||||||
|
parent_branch = parent_task.get("branch_name")
|
||||||
|
if not parent_branch:
|
||||||
|
return format_error_response(
|
||||||
|
"PARENT_BRANCH_REQUIRED",
|
||||||
|
"Parent task must have a branch before creating git subtasks.",
|
||||||
|
{
|
||||||
|
"parent_task_id": parent_task["id"],
|
||||||
|
"parent_status": parent_task.get("status"),
|
||||||
|
},
|
||||||
|
hint="Claim the parent task first. Branch is auto-created on claim.",
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = _build_task_payload(input_data, project_id, parent_task)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
create_resp = await client.post("/tasks", json=payload)
|
create_resp = await client.post("/tasks", json=payload)
|
||||||
@@ -445,6 +536,57 @@ async def handle_task_assign(
|
|||||||
if validation_error:
|
if validation_error:
|
||||||
return validation_error
|
return validation_error
|
||||||
|
|
||||||
|
# ENFORCEMENT: Block Cell PM from directly assigning devs on complex tasks
|
||||||
|
# Cell PM must create subtasks first for medium+ complexity work
|
||||||
|
assignee_role = get_agent_role(input_data.assignee)
|
||||||
|
if role == "cell_pm" and assignee_role == "developer":
|
||||||
|
complexity = task.get("estimated_complexity", "low")
|
||||||
|
if complexity in ("medium", "high", "critical"):
|
||||||
|
# Check if task already has subtasks
|
||||||
|
try:
|
||||||
|
subtasks_resp = await client.get(
|
||||||
|
f"/tasks/{input_data.task_id}/subtasks"
|
||||||
|
)
|
||||||
|
subtasks = subtasks_resp.json() if subtasks_resp.ok else []
|
||||||
|
except Exception:
|
||||||
|
subtasks = []
|
||||||
|
|
||||||
|
# Also allow if this task IS a subtask (has parent)
|
||||||
|
is_subtask = task.get("parent_task_id") is not None
|
||||||
|
|
||||||
|
if not subtasks and not is_subtask:
|
||||||
|
return format_error_response(
|
||||||
|
"SUBTASK_REQUIRED",
|
||||||
|
f"Cannot assign {complexity} complexity task directly to dev. "
|
||||||
|
"Cell PM must break down the work into subtasks first.",
|
||||||
|
{
|
||||||
|
"task_id": task.get("id"),
|
||||||
|
"complexity": complexity,
|
||||||
|
"guidance": (
|
||||||
|
f"Create subtasks with: roboco_task_create("
|
||||||
|
f"parent_task_id='{task.get('id')}', ...) "
|
||||||
|
"Then assign each subtask to developers."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# GUARDRAIL: Git tasks need branch before assigning to developers
|
||||||
|
if task.get("requires_git") and not task.get("branch_name"):
|
||||||
|
if assignee_role == "developer":
|
||||||
|
return format_error_response(
|
||||||
|
"NO_BRANCH_FOR_GIT_TASK",
|
||||||
|
"Git task must have a branch before assigning to developer.",
|
||||||
|
{
|
||||||
|
"task_id": task.get("id"),
|
||||||
|
"requires_git": True,
|
||||||
|
"has_branch": False,
|
||||||
|
},
|
||||||
|
hint=(
|
||||||
|
"Either claim the task first (creates branch), "
|
||||||
|
"or create subtasks for developers."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
assigned_task, assign_error = await assign_task_to_agent(
|
assigned_task, assign_error = await assign_task_to_agent(
|
||||||
client, input_data.task_id, input_data.assignee
|
client, input_data.task_id, input_data.assignee
|
||||||
)
|
)
|
||||||
@@ -565,6 +707,57 @@ async def handle_task_escalate(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _validate_activation_sequence(
|
||||||
|
client: ApiClient, task_id: str
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""Ensure parent is in_progress before activating subtask."""
|
||||||
|
task_resp = await client.get(f"/tasks/{task_id}")
|
||||||
|
if not task_resp.ok:
|
||||||
|
return None
|
||||||
|
|
||||||
|
task = task_resp.json()
|
||||||
|
parent_id = task.get("parent_task_id")
|
||||||
|
if not parent_id:
|
||||||
|
return None # Root task
|
||||||
|
|
||||||
|
parent_resp = await client.get(f"/tasks/{parent_id}")
|
||||||
|
if not parent_resp.ok:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parent = parent_resp.json()
|
||||||
|
parent_status = parent.get("status")
|
||||||
|
|
||||||
|
# Parent must be in_progress or paused (PM has started work)
|
||||||
|
if parent_status not in ["in_progress", "paused"]:
|
||||||
|
return format_error_response(
|
||||||
|
"PARENT_NOT_STARTED",
|
||||||
|
f"Cannot activate - parent task is '{parent_status}'.",
|
||||||
|
{"parent_id": parent_id, "parent_status": parent_status},
|
||||||
|
hint="Call roboco_task_start() on parent task first.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_activate_response(resp: Any, task_id: str) -> dict[str, Any] | None:
|
||||||
|
"""Handle API response for activation. Returns error dict or None if success."""
|
||||||
|
if resp.is_status(status.HTTP_404_NOT_FOUND):
|
||||||
|
return format_error_response("NOT_FOUND", f"Task {task_id} not found")
|
||||||
|
|
||||||
|
if resp.is_status(status.HTTP_400_BAD_REQUEST):
|
||||||
|
detail = resp.json().get("detail", "Activation failed")
|
||||||
|
return format_error_response("ACTIVATION_FAILED", detail)
|
||||||
|
|
||||||
|
if not resp.ok:
|
||||||
|
return format_error_response(
|
||||||
|
"ACTIVATION_FAILED",
|
||||||
|
"Failed to activate task",
|
||||||
|
{"status_code": resp.status_code, "detail": resp.text},
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def handle_task_activate(
|
async def handle_task_activate(
|
||||||
client: ApiClient, task_id: str, agent_id: str
|
client: ApiClient, task_id: str, agent_id: str
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -584,6 +777,11 @@ async def handle_task_activate(
|
|||||||
{"role": get_agent_role(agent_id)},
|
{"role": get_agent_role(agent_id)},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: Re-enable when sequence workflow is refined
|
||||||
|
# Validate parent is started before activating subtask
|
||||||
|
# if error := await _validate_activation_sequence(client, task_id):
|
||||||
|
# return error
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = await client.post(f"/tasks/{task_id}/activate")
|
resp = await client.post(f"/tasks/{task_id}/activate")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -592,19 +790,8 @@ async def handle_task_activate(
|
|||||||
f"Failed to connect to API: {type(e).__name__}",
|
f"Failed to connect to API: {type(e).__name__}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if resp.is_status(status.HTTP_404_NOT_FOUND):
|
if error := _handle_activate_response(resp, task_id):
|
||||||
return format_error_response("NOT_FOUND", f"Task {task_id} not found")
|
return error
|
||||||
|
|
||||||
if resp.is_status(status.HTTP_400_BAD_REQUEST):
|
|
||||||
detail = resp.json().get("detail", "Activation failed")
|
|
||||||
return format_error_response("ACTIVATION_FAILED", detail)
|
|
||||||
|
|
||||||
if not resp.ok:
|
|
||||||
return format_error_response(
|
|
||||||
"ACTIVATION_FAILED",
|
|
||||||
"Failed to activate task",
|
|
||||||
{"status_code": resp.status_code, "detail": resp.text},
|
|
||||||
)
|
|
||||||
|
|
||||||
task = resp.json()
|
task = resp.json()
|
||||||
guidance = (
|
guidance = (
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ async def handle_session_create_for_tasks(
|
|||||||
"scope": input_data.scope,
|
"scope": input_data.scope,
|
||||||
"relationship_type": input_data.relationship_type,
|
"relationship_type": input_data.relationship_type,
|
||||||
}
|
}
|
||||||
|
if input_data.group_id:
|
||||||
|
payload["group_id"] = input_data.group_id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = await client.post("/sessions/for-tasks", json=payload)
|
resp = await client.post("/sessions/for-tasks", json=payload)
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ async def _safe_checkout(
|
|||||||
"CHECKOUT_FAILED",
|
"CHECKOUT_FAILED",
|
||||||
f"Failed to checkout branch '{branch_name}'",
|
f"Failed to checkout branch '{branch_name}'",
|
||||||
{"status_code": checkout_resp.status_code, "detail": checkout_resp.text},
|
{"status_code": checkout_resp.status_code, "detail": checkout_resp.text},
|
||||||
hint="Branch may not exist yet. Ask PM to create it.",
|
hint="Branch may not exist. Re-claim or check if parent needs claiming.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Success - no error
|
# Success - no error
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ class Task:
|
|||||||
task_type: TaskType # code, documentation, research, planning, design, administrative
|
task_type: TaskType # code, documentation, research, planning, design, administrative
|
||||||
requires_git: bool # Whether git workflow applies
|
requires_git: bool # Whether git workflow applies
|
||||||
|
|
||||||
# Project & Branch (set by PM during setup)
|
# Project & Branch (branch auto-created on claim)
|
||||||
project_id: UUID | None
|
project_id: UUID | None
|
||||||
branch_name: str | None
|
branch_name: str | None
|
||||||
work_session_id: UUID | None
|
work_session_id: UUID | None
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ class Team(str, Enum):
|
|||||||
BACKEND = "backend"
|
BACKEND = "backend"
|
||||||
FRONTEND = "frontend"
|
FRONTEND = "frontend"
|
||||||
UX_UI = "ux_ui"
|
UX_UI = "ux_ui"
|
||||||
MAIN_PM = "main_pm" # Main PM level - cross-cell coordination
|
FULLSTACK = "fullstack"
|
||||||
|
MAIN_PM = "main_pm"
|
||||||
BOARD = "board"
|
BOARD = "board"
|
||||||
MARKETING = "marketing"
|
MARKETING = "marketing"
|
||||||
|
|
||||||
|
|||||||
@@ -198,6 +198,9 @@ class SessionForTasksCreate(RobocoBase):
|
|||||||
|
|
||||||
task_ids: list[UUID] = Field(..., min_length=1, description="Tasks to link")
|
task_ids: list[UUID] = Field(..., min_length=1, description="Tasks to link")
|
||||||
channel_slug: str = Field(..., description="Channel where session is created")
|
channel_slug: str = Field(..., description="Channel where session is created")
|
||||||
|
group_id: UUID | None = Field(
|
||||||
|
default=None, description="Optional group ID to place session under"
|
||||||
|
)
|
||||||
scope: SessionScope = Field(
|
scope: SessionScope = Field(
|
||||||
default=SessionScope.CELL,
|
default=SessionScope.CELL,
|
||||||
description="Session scope level for context loading strategy",
|
description="Session scope level for context loading strategy",
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class Task(TimestampMixin):
|
|||||||
default=True, description="Whether this task requires git workflow"
|
default=True, description="Whether this task requires git workflow"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Project & Branch (set by PM during setup)
|
# Project & Branch (branch auto-created on claim)
|
||||||
project_id: UUID | None = Field(
|
project_id: UUID | None = Field(
|
||||||
default=None, description="Project this task works on"
|
default=None, description="Project this task works on"
|
||||||
)
|
)
|
||||||
|
|||||||
+245
-42
@@ -18,7 +18,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, ClassVar
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
@@ -910,7 +910,7 @@ class AgentOrchestrator:
|
|||||||
# Role-based permissions enforced at handler level:
|
# Role-based permissions enforced at handler level:
|
||||||
# - All agents: read-only (status, log, diff, branch list)
|
# - All agents: read-only (status, log, diff, branch list)
|
||||||
# - Developers: commit, push, create PR
|
# - Developers: commit, push, create PR
|
||||||
# - PMs: create branch, checkout, merge PR
|
# - PMs: checkout, merge PR (branches auto-created on claim)
|
||||||
mcp_servers["roboco-git"] = {
|
mcp_servers["roboco-git"] = {
|
||||||
"command": "uv",
|
"command": "uv",
|
||||||
"args": [
|
"args": [
|
||||||
@@ -1449,6 +1449,113 @@ Start by:
|
|||||||
return False
|
return False
|
||||||
return self._instances[agent_id].state == AgentState.ACTIVE
|
return self._instances[agent_id].state == AgentState.ACTIVE
|
||||||
|
|
||||||
|
async def _validate_task_for_spawn(
|
||||||
|
self,
|
||||||
|
client: httpx.AsyncClient,
|
||||||
|
task: dict,
|
||||||
|
agent_slug: str,
|
||||||
|
) -> str | None:
|
||||||
|
"""
|
||||||
|
Validate task is ready for agent spawn.
|
||||||
|
|
||||||
|
Returns None if valid, or error message if task cannot proceed.
|
||||||
|
This prevents spawning agents on tasks that are missing prerequisites.
|
||||||
|
"""
|
||||||
|
task_id = task.get("id")
|
||||||
|
if not task_id:
|
||||||
|
return "Task missing ID"
|
||||||
|
min_description_len = 10
|
||||||
|
|
||||||
|
# VALIDATION 1: Check description is not empty/trivial
|
||||||
|
description = (task.get("description") or "").strip()
|
||||||
|
if len(description) < min_description_len:
|
||||||
|
return (
|
||||||
|
f"Task {task_id} has inadequate description ({len(description)} chars)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# VALIDATION 2: For git tasks, check project + parent branch requirements
|
||||||
|
requires_git = task.get("requires_git", False)
|
||||||
|
if requires_git:
|
||||||
|
# Must have project
|
||||||
|
if not task.get("project_id"):
|
||||||
|
await self._auto_block_task(
|
||||||
|
client, task_id, "Git task needs project_id"
|
||||||
|
)
|
||||||
|
return f"Task {task_id} needs project"
|
||||||
|
|
||||||
|
# For subtasks, parent must have branch (for forking)
|
||||||
|
parent_id = task.get("parent_task_id")
|
||||||
|
if parent_id:
|
||||||
|
parent_resp = await client.get(f"{self._api_url}/tasks/{parent_id}")
|
||||||
|
if parent_resp.is_success:
|
||||||
|
parent = parent_resp.json()
|
||||||
|
if parent.get("requires_git") and not parent.get("branch_name"):
|
||||||
|
await self._auto_block_task(
|
||||||
|
client,
|
||||||
|
task_id,
|
||||||
|
"Parent task must be claimed first to create its branch",
|
||||||
|
)
|
||||||
|
return f"Task {task_id} waiting for parent branch"
|
||||||
|
|
||||||
|
# Root task or parent has branch - branch will auto-create on claim
|
||||||
|
logger.info(
|
||||||
|
"Git task ready for hierarchical branch creation", task_id=task_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# VALIDATION 3: Check complexity vs subtasks for devs
|
||||||
|
from roboco.agents_config import get_agent_role
|
||||||
|
|
||||||
|
agent_role = get_agent_role(agent_slug)
|
||||||
|
if agent_role == "developer":
|
||||||
|
complexity = task.get("estimated_complexity", "low")
|
||||||
|
parent_task_id = task.get("parent_task_id")
|
||||||
|
|
||||||
|
if complexity in ("medium", "high", "critical") and not parent_task_id:
|
||||||
|
# Check if this task has subtasks
|
||||||
|
try:
|
||||||
|
resp = await client.get(f"{self._api_url}/tasks/{task_id}/subtasks")
|
||||||
|
subtasks = resp.json() if resp.is_success else []
|
||||||
|
except Exception:
|
||||||
|
subtasks = []
|
||||||
|
|
||||||
|
if not subtasks:
|
||||||
|
await self._auto_block_task(
|
||||||
|
client,
|
||||||
|
task_id,
|
||||||
|
f"Task complexity is {complexity} but no subtasks. "
|
||||||
|
"Cell PM must break down work first.",
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"Task {task_id} is {complexity} complexity "
|
||||||
|
"without subtasks - Cell PM must break it down"
|
||||||
|
)
|
||||||
|
|
||||||
|
return None # All validations passed
|
||||||
|
|
||||||
|
async def _auto_block_task(
|
||||||
|
self, client: httpx.AsyncClient, task_id: str, reason: str
|
||||||
|
) -> None:
|
||||||
|
"""Auto-block a task that cannot proceed due to missing prerequisites."""
|
||||||
|
try:
|
||||||
|
await client.patch(
|
||||||
|
f"{self._api_url}/tasks/{task_id}",
|
||||||
|
json={
|
||||||
|
"status": "blocked",
|
||||||
|
"dev_notes": f"[AUTO-BLOCKED] {reason}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Auto-blocked task with missing prerequisites",
|
||||||
|
task_id=task_id,
|
||||||
|
reason=reason,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
"Failed to auto-block task",
|
||||||
|
task_id=task_id,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
def _select_agent_for_cell(self, cell: str, role: str) -> str | None:
|
def _select_agent_for_cell(self, cell: str, role: str) -> str | None:
|
||||||
"""
|
"""
|
||||||
Select the best available agent for a cell and role.
|
Select the best available agent for a cell and role.
|
||||||
@@ -1546,7 +1653,7 @@ Start by:
|
|||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Fetch notifications by type."""
|
"""Fetch notifications by type."""
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
"type": notification_type,
|
"type_filter": notification_type,
|
||||||
"pending_ack_only": str(unacknowledged).lower(),
|
"pending_ack_only": str(unacknowledged).lower(),
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
@@ -1638,39 +1745,49 @@ Start by:
|
|||||||
|
|
||||||
def _classify_task_routing(self, task: dict[str, Any]) -> str:
|
def _classify_task_routing(self, task: dict[str, Any]) -> str:
|
||||||
"""
|
"""
|
||||||
Classify a task for routing based on team, complexity, and keywords.
|
Classify a task for routing based on task_type, team, complexity, and keywords.
|
||||||
|
|
||||||
Returns one of: "board", "main_pm", "cell_pm", "dev", "marketing"
|
Returns one of: "board", "main_pm", "cell_pm", "dev", "marketing"
|
||||||
"""
|
"""
|
||||||
team = task.get("team")
|
team = task.get("team")
|
||||||
|
task_type = task.get("task_type", "code")
|
||||||
|
cell_teams = ("backend", "frontend", "ux_ui")
|
||||||
|
result: str | None = None
|
||||||
|
|
||||||
# Explicit team assignment takes precedence
|
# Task type takes precedence for non-code work
|
||||||
if team in self._TEAM_ROUTING_MAP:
|
if task_type in ("planning", "research", "administrative"):
|
||||||
return self._TEAM_ROUTING_MAP[team]
|
result = "cell_pm" if team in cell_teams else "main_pm"
|
||||||
|
elif task_type == "design" and team not in ("backend", "frontend"):
|
||||||
|
result = "cell_pm"
|
||||||
|
elif task_type == "documentation" and not task.get("requires_git"):
|
||||||
|
result = "cell_pm" if team in cell_teams else "main_pm"
|
||||||
|
elif team in self._TEAM_ROUTING_MAP:
|
||||||
|
result = self._TEAM_ROUTING_MAP[team]
|
||||||
|
|
||||||
# For cell teams, use keyword/complexity analysis
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Keyword/complexity analysis for code tasks without explicit routing
|
||||||
title = (task.get("title") or "").lower()
|
title = (task.get("title") or "").lower()
|
||||||
description = (task.get("description") or "").lower()
|
description = (task.get("description") or "").lower()
|
||||||
text = f"{title} {description}"
|
text = f"{title} {description}"
|
||||||
complexity = task.get("estimated_complexity", "medium").lower()
|
complexity = task.get("estimated_complexity", "medium").lower()
|
||||||
|
|
||||||
# Board-level keywords → Board
|
|
||||||
if self._has_board_keywords(text):
|
if self._has_board_keywords(text):
|
||||||
return "board"
|
return "board"
|
||||||
|
|
||||||
# Cross-cell keywords (e.g., "all teams") → Main PM (regardless of complexity)
|
needs_main_pm = (
|
||||||
if self._has_cross_cell_keywords(text):
|
self._has_cross_cell_keywords(text)
|
||||||
|
or complexity in ("high", "critical")
|
||||||
|
or not team
|
||||||
|
or team == "all"
|
||||||
|
)
|
||||||
|
if needs_main_pm:
|
||||||
return "main_pm"
|
return "main_pm"
|
||||||
|
|
||||||
# High complexity or cross-team → Main PM
|
|
||||||
if complexity in ("high", "critical") or not team or team == "all":
|
|
||||||
return "main_pm"
|
|
||||||
|
|
||||||
# PM keywords or medium complexity → Cell PM
|
|
||||||
if self._has_pm_keywords(text) or complexity == "medium":
|
if self._has_pm_keywords(text) or complexity == "medium":
|
||||||
return "cell_pm"
|
return "cell_pm"
|
||||||
|
|
||||||
# Low complexity, single team → Direct to dev
|
|
||||||
return "dev"
|
return "dev"
|
||||||
|
|
||||||
# Team to PM mapping for routing
|
# Team to PM mapping for routing
|
||||||
@@ -1958,6 +2075,9 @@ Start now: roboco_task_get("{task_id}")
|
|||||||
# Scheduled dispatchers
|
# Scheduled dispatchers
|
||||||
await self._dispatch_audit_work(client)
|
await self._dispatch_audit_work(client)
|
||||||
|
|
||||||
|
# Proactive enforcement - detect and block stuck tasks
|
||||||
|
await self._detect_stuck_tasks(client)
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# SMART DISPATCHER - TASK-BASED DISPATCHERS
|
# SMART DISPATCHER - TASK-BASED DISPATCHERS
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -2254,6 +2374,21 @@ Begin with step 1: roboco_task_get("{task_id}")
|
|||||||
# For pending tasks that ARE already assigned (by PM),
|
# For pending tasks that ARE already assigned (by PM),
|
||||||
# spawn the assigned agent with the appropriate prompt
|
# spawn the assigned agent with the appropriate prompt
|
||||||
if agent_slug and not self._is_agent_active(agent_slug):
|
if agent_slug and not self._is_agent_active(agent_slug):
|
||||||
|
# PRE-SPAWN VALIDATION: Check task readiness before spawning
|
||||||
|
# This prevents spawning agents on tasks that can't proceed
|
||||||
|
validation_issue = await self._validate_task_for_spawn(
|
||||||
|
client, task, agent_slug
|
||||||
|
)
|
||||||
|
if validation_issue:
|
||||||
|
# Log and skip - don't spawn on invalid tasks
|
||||||
|
logger.warning(
|
||||||
|
"Skipping spawn due to validation failure",
|
||||||
|
task_id=task["id"],
|
||||||
|
agent=agent_slug,
|
||||||
|
reason=validation_issue,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
await self.spawn_agent(
|
await self.spawn_agent(
|
||||||
agent_id=agent_slug,
|
agent_id=agent_slug,
|
||||||
task_id=task["id"],
|
task_id=task["id"],
|
||||||
@@ -2561,6 +2696,98 @@ Begin with step 1: roboco_task_get("{task_id}")
|
|||||||
# TODO: Add scheduled periodic audits
|
# TODO: Add scheduled periodic audits
|
||||||
# Check last audit time, spawn if overdue
|
# Check last audit time, spawn if overdue
|
||||||
|
|
||||||
|
async def _detect_stuck_tasks(self, client: httpx.AsyncClient) -> None:
|
||||||
|
"""
|
||||||
|
Detect and auto-block tasks that are stuck.
|
||||||
|
|
||||||
|
This is a proactive enforcement mechanism that finds tasks which
|
||||||
|
have been pending without progress and have prerequisite issues.
|
||||||
|
Runs every dispatcher cycle but only takes action on truly stuck tasks.
|
||||||
|
|
||||||
|
CEO-approved timeout: 10 minutes
|
||||||
|
"""
|
||||||
|
STUCK_THRESHOLD_MINUTES = 10 # CEO-approved threshold
|
||||||
|
|
||||||
|
tasks = await self._fetch_tasks(client, "pending")
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
age = self._get_task_age(task)
|
||||||
|
if age is None or age < timedelta(minutes=STUCK_THRESHOLD_MINUTES):
|
||||||
|
continue
|
||||||
|
|
||||||
|
issues = self._check_stuck_conditions(task)
|
||||||
|
issues.extend(await self._check_dev_subtask_issue(client, task))
|
||||||
|
|
||||||
|
if issues:
|
||||||
|
task_id = task.get("id")
|
||||||
|
if not task_id:
|
||||||
|
continue
|
||||||
|
age_mins = int(age.total_seconds() // 60)
|
||||||
|
reason = f"Task stuck for {age_mins} minutes: " + ", ".join(issues)
|
||||||
|
await self._auto_block_task(client, task_id, reason)
|
||||||
|
logger.warning(
|
||||||
|
"Auto-blocked stuck task",
|
||||||
|
task_id=task_id,
|
||||||
|
age_minutes=age_mins,
|
||||||
|
issues=issues,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_task_age(self, task: dict[str, Any]) -> timedelta | None:
|
||||||
|
"""Parse task created_at and return age, or None if unparseable."""
|
||||||
|
created_at_str = task.get("created_at")
|
||||||
|
if not created_at_str:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
if created_at_str.endswith("Z"):
|
||||||
|
created_at_str = created_at_str[:-1] + "+00:00"
|
||||||
|
created_at = datetime.fromisoformat(created_at_str)
|
||||||
|
if created_at.tzinfo is None:
|
||||||
|
created_at = created_at.replace(tzinfo=UTC)
|
||||||
|
return datetime.now(UTC) - created_at
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
_MIN_DESCRIPTION_LEN = 10
|
||||||
|
|
||||||
|
def _check_stuck_conditions(self, task: dict[str, Any]) -> list[str]:
|
||||||
|
"""Check for common stuck conditions (git, description)."""
|
||||||
|
issues: list[str] = []
|
||||||
|
if task.get("requires_git") and not task.get("branch_name"):
|
||||||
|
issues.append("Git task missing branch_name")
|
||||||
|
description = (task.get("description") or "").strip()
|
||||||
|
if len(description) < self._MIN_DESCRIPTION_LEN:
|
||||||
|
issues.append("Empty or inadequate description")
|
||||||
|
return issues
|
||||||
|
|
||||||
|
async def _check_dev_subtask_issue(
|
||||||
|
self, client: httpx.AsyncClient, task: dict[str, Any]
|
||||||
|
) -> list[str]:
|
||||||
|
"""Check if complex dev task is missing subtasks."""
|
||||||
|
from roboco.agents_config import get_agent_role
|
||||||
|
|
||||||
|
assigned_to = task.get("assigned_to")
|
||||||
|
if not assigned_to:
|
||||||
|
return []
|
||||||
|
|
||||||
|
agent_slug = self._resolve_agent_slug(assigned_to)
|
||||||
|
if not agent_slug or get_agent_role(agent_slug) != "developer":
|
||||||
|
return []
|
||||||
|
|
||||||
|
complexity = task.get("estimated_complexity", "low")
|
||||||
|
is_low_complexity = complexity not in ("medium", "high", "critical")
|
||||||
|
if is_low_complexity or task.get("parent_task_id"):
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = await client.get(f"{self._api_url}/tasks/{task.get('id')}/subtasks")
|
||||||
|
subtasks = resp.json() if resp.is_success else []
|
||||||
|
except Exception:
|
||||||
|
subtasks = []
|
||||||
|
|
||||||
|
if not subtasks:
|
||||||
|
return [f"{complexity} complexity task without subtasks"]
|
||||||
|
return []
|
||||||
|
|
||||||
async def _dispatch_a2a_work(self, client: httpx.AsyncClient) -> None:
|
async def _dispatch_a2a_work(self, client: httpx.AsyncClient) -> None:
|
||||||
"""
|
"""
|
||||||
Dispatch A2A (Agent-to-Agent) requests to target agents.
|
Dispatch A2A (Agent-to-Agent) requests to target agents.
|
||||||
@@ -2597,16 +2824,12 @@ Begin with step 1: roboco_task_get("{task_id}")
|
|||||||
self,
|
self,
|
||||||
status: str,
|
status: str,
|
||||||
has_plan: bool,
|
has_plan: bool,
|
||||||
requires_git: bool,
|
|
||||||
branch_name: str | None,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Determine developer workflow state from task attributes.
|
"""Determine developer workflow state from task attributes.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
status: Task status (claimed, in_progress, needs_revision, etc.)
|
status: Task status (claimed, in_progress, needs_revision, etc.)
|
||||||
has_plan: Whether task has a plan submitted
|
has_plan: Whether task has a plan submitted
|
||||||
requires_git: Whether task requires git workflow
|
|
||||||
branch_name: Branch name if git task (PM creates this)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Workflow state string (NEEDS_PLAN, READY_TO_START, EXECUTING, etc.)
|
Workflow state string (NEEDS_PLAN, READY_TO_START, EXECUTING, etc.)
|
||||||
@@ -2625,8 +2848,6 @@ Begin with step 1: roboco_task_get("{task_id}")
|
|||||||
if status == "claimed":
|
if status == "claimed":
|
||||||
if not has_plan:
|
if not has_plan:
|
||||||
return "NEEDS_PLAN"
|
return "NEEDS_PLAN"
|
||||||
if requires_git and not branch_name:
|
|
||||||
return "WAITING_FOR_BRANCH"
|
|
||||||
return "READY_TO_START"
|
return "READY_TO_START"
|
||||||
|
|
||||||
return status.upper()
|
return status.upper()
|
||||||
@@ -2657,20 +2878,6 @@ Call roboco_task_plan("{task_id}", {{
|
|||||||
}})
|
}})
|
||||||
|
|
||||||
You CANNOT call roboco_task_start() until plan is submitted.
|
You CANNOT call roboco_task_start() until plan is submitted.
|
||||||
""",
|
|
||||||
"WAITING_FOR_BRANCH": """## BLOCKED: Waiting for Branch
|
|
||||||
|
|
||||||
Your plan is approved, but this is a git task and no branch has been created yet.
|
|
||||||
|
|
||||||
The PM must create a branch for you using:
|
|
||||||
`roboco_git_create_branch(project_slug, task_id, branch_type)`
|
|
||||||
|
|
||||||
**What to do:**
|
|
||||||
1. Send a message to your PM requesting branch creation
|
|
||||||
2. Or escalate: `roboco_task_escalate(task_id, "Need branch created for git task")`
|
|
||||||
3. Wait for notification that branch is ready
|
|
||||||
|
|
||||||
You CANNOT call roboco_task_start() until branch_name is set on the task.
|
|
||||||
""",
|
""",
|
||||||
"READY_TO_START": f"""## NEXT STEP: Start Work
|
"READY_TO_START": f"""## NEXT STEP: Start Work
|
||||||
|
|
||||||
@@ -2716,11 +2923,7 @@ Run quality checks and verify against acceptance criteria:
|
|||||||
|
|
||||||
# Determine workflow state based on task attributes
|
# Determine workflow state based on task attributes
|
||||||
has_plan = bool(task.get("plan"))
|
has_plan = bool(task.get("plan"))
|
||||||
requires_git = task.get("requires_git", False)
|
workflow_state = self._get_workflow_state(status, has_plan)
|
||||||
branch_name = task.get("branch_name")
|
|
||||||
workflow_state = self._get_workflow_state(
|
|
||||||
status, has_plan, requires_git, branch_name
|
|
||||||
)
|
|
||||||
instructions = self._get_workflow_instructions(workflow_state, task_id)
|
instructions = self._get_workflow_instructions(workflow_state, task_id)
|
||||||
|
|
||||||
return f"""You have been assigned a development task.
|
return f"""You have been assigned a development task.
|
||||||
|
|||||||
@@ -367,8 +367,7 @@ class GitService(BaseService):
|
|||||||
if not result.stdout.strip():
|
if not result.stdout.strip():
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Parent branch '{base_branch}' does not exist on remote. "
|
f"Parent branch '{base_branch}' does not exist on remote. "
|
||||||
f"The parent task's branch must be created and pushed first. "
|
f"The parent task must be claimed first (claim creates branch)."
|
||||||
f"Ensure Main PM/Cell PM created their branches before this task."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create and push branch
|
# Create and push branch
|
||||||
|
|||||||
@@ -665,11 +665,19 @@ class MessagingService(BaseService):
|
|||||||
if not channel:
|
if not channel:
|
||||||
raise NotFoundError(f"Channel '{req.channel_slug}' not found")
|
raise NotFoundError(f"Channel '{req.channel_slug}' not found")
|
||||||
|
|
||||||
# Get first group in channel (or create default)
|
# Use provided group_id or fall back to first group in channel
|
||||||
groups = await self.list_groups_in_channel(cast("UUID", channel.id))
|
if req.group_id:
|
||||||
if not groups:
|
group_result = await self.session.execute(
|
||||||
raise ValueError(f"No groups found in channel '{req.channel_slug}'")
|
select(GroupTable).where(GroupTable.id == req.group_id)
|
||||||
group = groups[0]
|
)
|
||||||
|
group = group_result.scalar_one_or_none()
|
||||||
|
if not group:
|
||||||
|
raise NotFoundError(f"Group '{req.group_id}' not found")
|
||||||
|
else:
|
||||||
|
groups = await self.list_groups_in_channel(cast("UUID", channel.id))
|
||||||
|
if not groups:
|
||||||
|
raise ValueError(f"No groups found in channel '{req.channel_slug}'")
|
||||||
|
group = groups[0]
|
||||||
|
|
||||||
# Create session with config and scope
|
# Create session with config and scope
|
||||||
session_req = SessionCreateRequest(
|
session_req = SessionCreateRequest(
|
||||||
|
|||||||
+162
-2
@@ -21,7 +21,9 @@ from roboco.db.tables import (
|
|||||||
WorkSessionTable,
|
WorkSessionTable,
|
||||||
)
|
)
|
||||||
from roboco.enforcement import (
|
from roboco.enforcement import (
|
||||||
|
GitContext,
|
||||||
TaskOwnershipError,
|
TaskOwnershipError,
|
||||||
|
validate_git_requirements,
|
||||||
validate_task_ownership,
|
validate_task_ownership,
|
||||||
validate_task_transition,
|
validate_task_transition,
|
||||||
)
|
)
|
||||||
@@ -157,7 +159,8 @@ class TaskService(BaseService):
|
|||||||
Validate and set task status with lifecycle enforcement.
|
Validate and set task status with lifecycle enforcement.
|
||||||
|
|
||||||
This is the single point of truth for status changes. All transitions
|
This is the single point of truth for status changes. All transitions
|
||||||
are validated against VALID_TRANSITIONS and ROLE_RESTRICTED_TRANSITIONS.
|
are validated against VALID_TRANSITIONS, ROLE_RESTRICTED_TRANSITIONS,
|
||||||
|
and git requirements.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task: The task to update
|
task: The task to update
|
||||||
@@ -166,6 +169,7 @@ class TaskService(BaseService):
|
|||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TaskLifecycleError: If transition is invalid or role not permitted
|
TaskLifecycleError: If transition is invalid or role not permitted
|
||||||
|
GitRequirementError: If git requirements not met
|
||||||
"""
|
"""
|
||||||
current = (
|
current = (
|
||||||
task.status.value if isinstance(task.status, TaskStatus) else task.status
|
task.status.value if isinstance(task.status, TaskStatus) else task.status
|
||||||
@@ -175,6 +179,17 @@ class TaskService(BaseService):
|
|||||||
# Validate the transition (raises TaskLifecycleError if invalid)
|
# Validate the transition (raises TaskLifecycleError if invalid)
|
||||||
validate_task_transition(current, target, agent_role)
|
validate_task_transition(current, target, agent_role)
|
||||||
|
|
||||||
|
# Validate git requirements (raises GitRequirementError if not met)
|
||||||
|
if task.requires_git:
|
||||||
|
git_ctx = GitContext(
|
||||||
|
requires_git=True,
|
||||||
|
docs_complete=bool(task.docs_complete),
|
||||||
|
pr_created=bool(task.pr_created),
|
||||||
|
pr_number=task.pr_number,
|
||||||
|
branch_name=str(task.branch_name) if task.branch_name else None,
|
||||||
|
)
|
||||||
|
validate_git_requirements(current, target, git_ctx)
|
||||||
|
|
||||||
# Apply the status change
|
# Apply the status change
|
||||||
task.status = new_status
|
task.status = new_status
|
||||||
self.log.info(
|
self.log.info(
|
||||||
@@ -211,6 +226,10 @@ class TaskService(BaseService):
|
|||||||
status=req.status if req.status else TaskStatus.PENDING,
|
status=req.status if req.status else TaskStatus.PENDING,
|
||||||
sequence=req.sequence, # Task ordering within siblings
|
sequence=req.sequence, # Task ordering within siblings
|
||||||
dependency_ids=req.dependency_ids, # Task IDs that must complete first
|
dependency_ids=req.dependency_ids, # Task IDs that must complete first
|
||||||
|
# Git configuration - CRITICAL: These must be passed through
|
||||||
|
task_type=req.task_type,
|
||||||
|
requires_git=req.requires_git,
|
||||||
|
project_id=req.project_id,
|
||||||
)
|
)
|
||||||
self.session.add(task)
|
self.session.add(task)
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
@@ -325,6 +344,16 @@ class TaskService(BaseService):
|
|||||||
"before activating."
|
"before activating."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ENFORCEMENT: Git tasks require project_id before activation
|
||||||
|
if task.requires_git and not task.project_id:
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot activate task '{task.title}' - requires git but no project. "
|
||||||
|
"Fix: (1) Re-create task with project_slug, OR "
|
||||||
|
"(2) Set requires_git=False if git not needed."
|
||||||
|
)
|
||||||
|
|
||||||
|
# NOTE: Git branch is auto-created on claim, not required at activation
|
||||||
|
|
||||||
# Transition to PENDING
|
# Transition to PENDING
|
||||||
task.status = TaskStatus.PENDING
|
task.status = TaskStatus.PENDING
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
@@ -336,6 +365,120 @@ class TaskService(BaseService):
|
|||||||
)
|
)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
|
async def _ensure_branch_for_git_task(
|
||||||
|
self,
|
||||||
|
task: TaskTable,
|
||||||
|
agent_id: UUID,
|
||||||
|
) -> str:
|
||||||
|
"""Auto-create hierarchical branch for git tasks. Raises on failure.
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
- If branch exists: return it
|
||||||
|
- If no project: raise error
|
||||||
|
- Create NEW branch (hierarchical name built by build_branch_name)
|
||||||
|
- Branch created from parent's branch (or default if root)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If branch cannot be created (mandatory for git tasks)
|
||||||
|
"""
|
||||||
|
if not task.requires_git:
|
||||||
|
raise ValueError("Task does not require git")
|
||||||
|
|
||||||
|
if task.branch_name:
|
||||||
|
return str(task.branch_name)
|
||||||
|
|
||||||
|
if not task.project_id:
|
||||||
|
raise ValueError(
|
||||||
|
"Git task requires project_id to create branch. "
|
||||||
|
"Assign a project before claiming."
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self._auto_create_branch(task, agent_id)
|
||||||
|
|
||||||
|
async def _auto_create_branch(
|
||||||
|
self,
|
||||||
|
task: TaskTable,
|
||||||
|
agent_id: UUID,
|
||||||
|
) -> str:
|
||||||
|
"""Create hierarchical branch for git task. Raises on failure.
|
||||||
|
|
||||||
|
Branch naming (via build_branch_name):
|
||||||
|
- Root: feature/team/ROOT_ID
|
||||||
|
- Subtask: feature/team/ROOT_ID/SUB_ID
|
||||||
|
- Sub-subtask: feature/team/ROOT_ID/SUB_ID/SUBSUB_ID
|
||||||
|
|
||||||
|
Parent branch resolution:
|
||||||
|
- Subtask: uses parent task's branch_name
|
||||||
|
- Root: uses project's default branch (main/master)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If branch cannot be created
|
||||||
|
"""
|
||||||
|
from roboco.api.schemas.git import GitCreateBranchRequest
|
||||||
|
from roboco.services.git import get_git_service
|
||||||
|
from roboco.services.project import get_project_service
|
||||||
|
|
||||||
|
git_service = get_git_service(self.session)
|
||||||
|
project_service = get_project_service(self.session)
|
||||||
|
|
||||||
|
project = await project_service.get(UUID(str(task.project_id)))
|
||||||
|
if not project:
|
||||||
|
raise ValueError(f"Project {task.project_id} not found")
|
||||||
|
|
||||||
|
parent_branch: str | None = None
|
||||||
|
if task.parent_task_id:
|
||||||
|
parent_task = await self.get(UUID(str(task.parent_task_id)))
|
||||||
|
if parent_task and parent_task.branch_name:
|
||||||
|
parent_branch = str(parent_task.branch_name)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Parent task must be claimed first. "
|
||||||
|
"Subtasks fork from parent branch (auto-created on claim)."
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = await git_service.get_workspace(project.slug, agent_id)
|
||||||
|
|
||||||
|
project_cell = (
|
||||||
|
project.assigned_cell.value
|
||||||
|
if project.assigned_cell and hasattr(project.assigned_cell, "value")
|
||||||
|
else str(project.assigned_cell)
|
||||||
|
if project.assigned_cell
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
task_team = (
|
||||||
|
task.team.value if task.team and hasattr(task.team, "value") else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if project_cell == "fullstack":
|
||||||
|
team = f"{project.slug}/{task_team or 'cross'}"
|
||||||
|
elif project_cell:
|
||||||
|
team = project_cell
|
||||||
|
elif task_team:
|
||||||
|
team = task_team
|
||||||
|
else:
|
||||||
|
team = "cross"
|
||||||
|
|
||||||
|
request = GitCreateBranchRequest(
|
||||||
|
task_id=str(task.id),
|
||||||
|
project_slug=project.slug,
|
||||||
|
branch_type="feature",
|
||||||
|
agent_id=str(agent_id),
|
||||||
|
parent_branch=parent_branch,
|
||||||
|
)
|
||||||
|
|
||||||
|
branch_name, _ = await git_service.create_branch(workspace, team, request)
|
||||||
|
|
||||||
|
task.branch_name = branch_name
|
||||||
|
await self.session.flush()
|
||||||
|
|
||||||
|
self.log.info(
|
||||||
|
"Auto-created hierarchical branch",
|
||||||
|
task_id=str(task.id),
|
||||||
|
branch_name=branch_name,
|
||||||
|
parent_branch=parent_branch or "default",
|
||||||
|
)
|
||||||
|
return branch_name
|
||||||
|
|
||||||
async def get(self, task_id: UUID) -> TaskTable | None:
|
async def get(self, task_id: UUID) -> TaskTable | None:
|
||||||
"""Get a task by ID."""
|
"""Get a task by ID."""
|
||||||
result = await self.session.execute(
|
result = await self.session.execute(
|
||||||
@@ -544,7 +687,14 @@ class TaskService(BaseService):
|
|||||||
|
|
||||||
await self.session.flush()
|
await self.session.flush()
|
||||||
|
|
||||||
|
# Auto-create hierarchical branch for git tasks (mandatory - raises on failure)
|
||||||
|
# Must happen BEFORE work session creation (work session needs branch_name)
|
||||||
|
if task.requires_git and not task.branch_name:
|
||||||
|
await self._ensure_branch_for_git_task(task, agent_id)
|
||||||
|
await self.session.refresh(task)
|
||||||
|
|
||||||
# Create work session for git-enabled tasks claimed by developers
|
# Create work session for git-enabled tasks claimed by developers
|
||||||
|
# (now branch exists, so work session can be created)
|
||||||
await self._create_work_session_if_needed(task, agent_id, agent_role)
|
await self._create_work_session_if_needed(task, agent_id, agent_role)
|
||||||
|
|
||||||
# Trigger proactive knowledge injection (fire-and-forget)
|
# Trigger proactive knowledge injection (fire-and-forget)
|
||||||
@@ -624,7 +774,7 @@ class TaskService(BaseService):
|
|||||||
Only creates a session if:
|
Only creates a session if:
|
||||||
- Task requires git (requires_git=True)
|
- Task requires git (requires_git=True)
|
||||||
- Task has a project_id set
|
- Task has a project_id set
|
||||||
- Task has a branch_name set (PM created the branch)
|
- Task has a branch_name set (auto-created on claim)
|
||||||
- Agent is a developer (not QA/Documenter claiming for review)
|
- Agent is a developer (not QA/Documenter claiming for review)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -2126,6 +2276,16 @@ class TaskService(BaseService):
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# ENFORCEMENT: Git tasks must have PR created before CEO approval
|
||||||
|
if task.requires_git and not task.pr_number:
|
||||||
|
self.log.warning(
|
||||||
|
"Cannot escalate to CEO - git task has no PR",
|
||||||
|
task_id=str(task_id),
|
||||||
|
requires_git=task.requires_git,
|
||||||
|
pr_created=task.pr_created,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
# Store escalation notes
|
# Store escalation notes
|
||||||
if notes:
|
if notes:
|
||||||
existing_context = task.quick_context or ""
|
existing_context = task.quick_context or ""
|
||||||
|
|||||||
Reference in New Issue
Block a user