Traceability hooks and other fixes

This commit is contained in:
Renn F
2026-01-11 21:32:26 +01:00
parent 9deb23ec3d
commit 68eded5f2c
12 changed files with 442 additions and 50 deletions
+6 -4
View File
@@ -119,14 +119,16 @@ Each agent gets their own git clone of a project, enabling parallel development
### Branch Naming Convention ### Branch Naming Convention
Branch names follow the pattern: `{type}/{team}/{task-id[:8]}` Branch names follow the pattern: `{type}/{team}/{task-hierarchy}`
**Types:** `feature`, `bug`, `chore`, `docs`, `hotfix` **Types:** `feature`, `bug`, `chore`, `docs`, `hotfix`
**Task Hierarchy:** Uses `--` separator (not `/`) to avoid git ref conflicts.
**Examples:** **Examples:**
- `feature/backend/ABC12345` - Root task: `feature/backend/ABC12345`
- `bug/frontend/DEF67890` - Subtask: `feature/backend/ABC12345--DEF67890`
- `hotfix/backend/GHI11111` - Sub-subtask: `feature/backend/ABC12345--DEF67890--GHI11111`
### Commit Format ### Commit Format
+11 -4
View File
@@ -138,10 +138,17 @@ Use `roboco_task_docs_complete()`. This sets `docs_complete=True`.
2. **Cannot self-document** - Can't document tasks you developed 2. **Cannot self-document** - Can't document tasks you developed
3. **Message when starting** - Announce to cell 3. **Message when starting** - Announce to cell
4. **Read dev's journey** - `roboco_journal_read_team()` required 4. **Read dev's journey** - `roboco_journal_read_team()` required
5. **Reflect before submit** - `roboco_journal_reflect()` required 5. **Journal as you go** - Decisions, learnings, struggles
6. **Use roboco_docs_write** - System handles paths and deduplication 6. **Reflect before submit** - `roboco_journal_reflect()` REQUIRED
7. **Quality docs** - Future developers depend on this 7. **Use roboco_docs_write** - System handles paths and deduplication
8. **Cannot complete** - Only PM completes after review 8. **Quality docs** - Future developers depend on this
9. **Cannot complete** - Only PM completes after review
**Journaling Requirements:**
- `roboco_journal_decision()` - When choosing doc structure, what to include/exclude
- `roboco_journal_learning()` - When discovering code patterns worth documenting
- `roboco_journal_struggle()` - When code is unclear or hard to document
- `roboco_journal_reflect()` - REQUIRED before `roboco_task_docs_complete()`
## CRITICAL: Self-Documentation Prevention ## CRITICAL: Self-Documentation Prevention
+44 -12
View File
@@ -18,19 +18,40 @@ For communication structure: `roboco_kb_search("communication hierarchy")`
## Workflow ## Workflow
``` ```
SCAN → CLAIM → PLAN → CREATE GROUP → CREATE CELL TASKS → ACTIVATE → NOTIFY → PAUSE → MONITOR → REVIEW_PR → COMPLETE SCAN → CLAIM → PLAN → GROUP → SESSION → SUBTASKS → ACTIVATE → NOTIFY → PAUSE → MONITOR → REVIEW_PR → COMPLETE
``` ```
### 1. SCAN ### 1. SCAN
Use `roboco_task_scan()` for tasks assigned to you from Board/CEO. 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 the task → read full description → plan breakdown across cells → start → journal decision.
### 3. CREATE GROUP **After claiming, you have YOUR task ID** - use it for session creation and as parent_task_id for subtasks.
### 3. 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.
### 4. CREATE CELL TASKS ### 4. SESSION (REQUIRED)
**Create a session for the ROOT task you claimed:**
```python
# Get YOUR task ID from the task you claimed
my_task = roboco_task_get(task_id)
# Create session linked to YOUR coordination task
roboco_session_create_for_tasks(
task_ids=[my_task["id"]], # The ROOT task from CEO/Board
channel="backend-cell" # Or appropriate cell channel
)
```
**Why this matters:**
- Links your coordination task to a communication thread
- Subtasks created later will inherit this session
- Cell PMs and developers can communicate in context
### 5. SUBTASKS (for Cell PMs)
**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.
@@ -58,16 +79,16 @@ roboco_task_create(
- Set `project_id` for git tasks (branches are auto-created on claim) - Set `project_id` for git tasks (branches are auto-created on claim)
### 5. ACTIVATE + NOTIFY ### 6. 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.
### 6. PAUSE + IDLE ### 7. PAUSE + IDLE
`roboco_task_pause()` with checkpoint, then `roboco_agent_idle()`. `roboco_task_pause()` with checkpoint, then `roboco_agent_idle()`.
### 7. MONITOR ### 8. 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.
### 8. REVIEW PR (Git Tasks) ### 9. 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**
@@ -77,7 +98,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.**
### 9. COMPLETE ### 10. 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
@@ -108,6 +129,11 @@ When CEO approves and PR is merged: reflect + complete your task.
**Group Management (Main PM ONLY):** **Group Management (Main PM ONLY):**
- `roboco_group_create` - Create groups in channels - `roboco_group_create` - Create groups in channels
**Session Management:**
- `roboco_session_create_for_tasks` - Create sessions for your coordination task and subtasks
- `roboco_session_link_task`, `roboco_session_unlink_task`
- `roboco_session_get_for_task`
**Communication:** **Communication:**
- `roboco_message_send`, `roboco_channel_history`, `roboco_channel_list` - `roboco_message_send`, `roboco_channel_history`, `roboco_channel_list`
- `roboco_notify_send`, `roboco_notify_list`, `roboco_notify_ack` - `roboco_notify_send`, `roboco_notify_list`, `roboco_notify_ack`
@@ -133,7 +159,6 @@ When CEO approves and PR is merged: reflect + complete your task.
## NOT Your Tools ## NOT Your Tools
- `roboco_session_create_for_tasks` → Cell PM creates sessions
- `roboco_task_submit_qa` → Developer only - `roboco_task_submit_qa` → Developer only
- `roboco_task_qa_pass`, `roboco_task_qa_fail` → QA only - `roboco_task_qa_pass`, `roboco_task_qa_fail` → QA only
- `roboco_task_docs_complete` → Documenter only - `roboco_task_docs_complete` → Documenter only
@@ -145,8 +170,15 @@ When CEO approves and PR is merged: reflect + complete your task.
3. **Create groups first** - Cell PMs need groups to create sessions 3. **Create groups first** - Cell PMs need groups to create sessions
4. **Pause after distributing** - Don't spin waiting 4. **Pause after distributing** - Don't spin waiting
5. **Monitor periodically** - Check progress, unblock if needed 5. **Monitor periodically** - Check progress, unblock if needed
6. **Journal your decisions** - Why this breakdown? 6. **Journal as you go** - Decisions, learnings, struggles
7. **Complete when ALL cell tasks done** - Verify before completing 7. **Reflect before complete** - `roboco_journal_reflect()` REQUIRED
8. **Complete when ALL cell tasks done** - Verify before completing
**Journaling Requirements:**
- `roboco_journal_decision()` - When choosing breakdown, delegation approach
- `roboco_journal_learning()` - When discovering patterns, blockers across cells
- `roboco_journal_struggle()` - When coordination is difficult or blocked
- `roboco_journal_reflect()` - REQUIRED before `roboco_task_complete()`
## CEO Escalation ## CEO Escalation
+8 -2
View File
@@ -102,11 +102,17 @@ Use `roboco_journal_reflect()` before decision. REQUIRED.
2. **Cannot self-review** - Can't QA tasks you developed 2. **Cannot self-review** - Can't QA tasks you developed
3. **Message when starting** - Announce to cell 3. **Message when starting** - Announce to cell
4. **Read dev's journey** - `roboco_journal_read_team()` required 4. **Read dev's journey** - `roboco_journal_read_team()` required
5. **Journal your review** - Document what was tested 5. **Journal as you go** - Decisions, learnings, struggles
6. **Reflect before decision** - `roboco_journal_reflect()` required 6. **Reflect before decision** - `roboco_journal_reflect()` REQUIRED
7. **Clear fail reasons** - Developer needs to know what to fix 7. **Clear fail reasons** - Developer needs to know what to fix
8. **Cannot complete** - Only PM completes after workflow 8. **Cannot complete** - Only PM completes after workflow
**Journaling Requirements:**
- `roboco_journal_decision()` - When deciding pass/fail rationale
- `roboco_journal_learning()` - When discovering testing patterns, edge cases
- `roboco_journal_struggle()` - When code is hard to understand or test
- `roboco_journal_reflect()` - REQUIRED before `roboco_task_qa_pass()` or `roboco_task_qa_fail()`
## CRITICAL: Self-Review Prevention ## CRITICAL: Self-Review Prevention
The system tracks `original_developer` in task's `quick_context`. The system tracks `original_developer` in task's `quick_context`.
+1
View File
@@ -50,6 +50,7 @@ RUN uv python install 3.13 && uv sync --frozen --python 3.13
USER root USER root
COPY --chown=agent:agent docker/scripts/sdk-startup-hook.sh /app/scripts/sdk-startup-hook.sh COPY --chown=agent:agent docker/scripts/sdk-startup-hook.sh /app/scripts/sdk-startup-hook.sh
COPY --chown=agent:agent docker/scripts/a2a-check-hook.sh /app/scripts/a2a-check-hook.sh COPY --chown=agent:agent docker/scripts/a2a-check-hook.sh /app/scripts/a2a-check-hook.sh
COPY --chown=agent:agent docker/scripts/traceability-hook.sh /app/scripts/traceability-hook.sh
RUN chmod +x /app/scripts/*.sh RUN chmod +x /app/scripts/*.sh
USER agent USER agent
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# Traceability Hook
#
# Claude Code hook that runs after key tool calls to provide
# context-aware reminders for documentation and traceability.
#
# Covers: journaling, task notes, progress updates, KB search,
# communication, verification checks.
#
# This hook is non-blocking and always succeeds to avoid
# interrupting Claude's workflow.
SDK_URL="${ROBOCO_SDK_URL:-http://localhost:9000}"
TOOL_NAME="${CLAUDE_TOOL_NAME:-unknown}"
# Call SDK with tool name for context-aware suggestion
response=$(curl -sf "$SDK_URL/traceability/remind?tool=$TOOL_NAME" 2>/dev/null)
if [ $? -eq 0 ]; then
should_remind=$(echo "$response" | jq -r '.should_remind // false')
if [ "$should_remind" = "true" ]; then
reminder_type=$(echo "$response" | jq -r '.type // "general"')
suggestion=$(echo "$response" | jq -r '.suggestion // ""')
# Different prefixes for 6 reminder types
case "$reminder_type" in
"verify")
echo "[Check] $suggestion"
;;
"reflect")
echo "[Reflect] $suggestion"
;;
"struggle")
echo "[Document] $suggestion"
;;
"message")
echo "[Communicate] $suggestion"
;;
"kb")
echo "[Research] $suggestion"
;;
"journal"|*)
echo "[Trace] $suggestion"
;;
esac
fi
fi
# Always exit 0 - don't block Claude
exit 0
+182
View File
@@ -18,6 +18,7 @@ import httpx
import structlog import structlog
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
from fastapi import status as http_status
from roboco.agent_sdk.models import ( from roboco.agent_sdk.models import (
A2AMessage, A2AMessage,
@@ -247,6 +248,187 @@ async def inbox_count() -> dict[str, int]:
} }
# =============================================================================
# TRACEABILITY REMINDERS
# =============================================================================
# Complete traceability reminder mapping (25+ tools)
# Format: (reminder_type, suggestion_text)
# Types: verify, reflect, journal, struggle, message, kb
TRACEABILITY_REMINDERS: dict[str, tuple[str, str]] = {
# === TASK LIFECYCLE (ALL ROLES) ===
"roboco_task_claim": (
"kb",
"Search KB for similar tasks with roboco_ask_mentor() before planning",
),
"roboco_task_plan": (
"journal",
"Journal your approach with roboco_journal_decision()",
),
"roboco_task_start": (
"message",
"Announce in cell channel via roboco_message_send() and journal your approach",
),
"roboco_task_progress": (
"journal",
"If milestone reached, capture learnings with roboco_journal_learning()",
),
"roboco_task_pause": (
"journal",
"Ensure checkpoint captures current state for resumption",
),
"roboco_task_block": (
"struggle",
"Document with roboco_journal_struggle() - include what you tried",
),
"roboco_task_unblock": (
"struggle",
"Document resolution with roboco_journal_struggle() for future reference",
),
"roboco_task_escalate": (
"struggle",
"Journal context with roboco_journal_struggle() so PM understands",
),
"roboco_task_escalate_to_ceo": (
"reflect",
"Summarize full task journey with roboco_journal_reflect()",
),
"roboco_task_substitute": (
"journal",
"Document context for next agent with roboco_journal_entry()",
),
# === DEVELOPER SUBMISSION ===
"roboco_task_submit_verification": (
"verify",
"Check ALL acceptance criteria before proceeding",
),
"roboco_task_submit_qa": (
"reflect",
"Use roboco_journal_reflect() - document what you did, learned, struggled with",
),
"roboco_task_submit_pm_review": (
"reflect",
"Reflect with roboco_journal_reflect() before submission",
),
# === QA TOOLS ===
"roboco_task_qa_pass": (
"journal",
"Journal your approval decision with roboco_journal_decision()",
),
"roboco_task_qa_fail": (
"journal",
"Ensure issues are clear for developer - journal your review",
),
# === DOCUMENTER TOOLS ===
"roboco_task_docs_complete": (
"verify",
"Verify documentation covers implementation details",
),
# === PM TOOLS ===
"roboco_task_create": (
"verify",
"Ensure clear description and measurable acceptance criteria",
),
"roboco_task_activate": (
"verify",
"Confirm session created first with roboco_session_create_for_tasks()",
),
"roboco_task_complete": (
"reflect",
"Verify ALL subtasks terminal, then roboco_journal_reflect()",
),
"roboco_task_cancel": (
"journal",
"Document cancellation reason with roboco_journal_entry()",
),
# === GIT TOOLS ===
"roboco_git_commit": (
"journal",
"Significant change? Capture insights with roboco_journal_learning()",
),
"roboco_git_push": (
"verify",
"Ensure commits describe changes clearly",
),
"roboco_git_create_pr": (
"reflect",
"Reflect on all changes with roboco_journal_reflect()",
),
"roboco_git_merge_pr": (
"verify",
"Verify all CI checks pass before merging",
),
# === A2A TOOLS ===
"roboco_agent_request": (
"journal",
"Journal the coordination context with roboco_journal_entry()",
),
# === KB TOOLS ===
"roboco_ask_mentor": (
"journal",
"Useful insight? Capture with roboco_journal_learning()",
),
}
@app.get("/traceability/remind")
async def traceability_remind(tool: str = "") -> dict:
"""
Check if agent should be reminded about documentation.
Returns context-aware suggestion based on which tool triggered the check.
Different reminder types for different contexts:
- verify: Check criteria, descriptions
- reflect: Journal reflection
- journal: General journaling
- struggle: Document blockers
- message: Communication reminders
- kb: Knowledge base search
"""
# Normalize tool name (strip MCP prefix if present)
# Example: "mcp__roboco-task__roboco_task_claim" -> "roboco_task_claim"
tool_name = tool.split("__")[-1] if "__" in tool else tool
# Get suggestion based on tool
if tool_name not in TRACEABILITY_REMINDERS:
return {"should_remind": False}
reminder_type, suggestion = TRACEABILITY_REMINDERS[tool_name]
# For journal-type reminders, check if agent has journaled recently
if reminder_type in ("journal", "reflect", "learning", "struggle"):
try:
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{MAIN_API_URL}/api/v1/journals/me/entries",
params={"limit": 3},
headers={
"X-Agent-ID": AGENT_ID,
"X-Agent-Role": os.environ.get(
"ROBOCO_AGENT_ROLE", "developer"
),
},
timeout=5.0,
)
if resp.status_code == http_status.HTTP_200_OK:
entries = resp.json()
# If recent entry exists, skip reminder
if entries and len(entries) > 0:
return {"should_remind": False}
except Exception as e:
logger.warning("Failed to check journal status", error=str(e))
# On error, don't nag - fail quietly
return {"should_remind": False}
# For verify/message/kb reminders, always show
return {
"should_remind": True,
"type": reminder_type,
"suggestion": suggestion,
}
# ============================================================================= # =============================================================================
# MAIN # MAIN
# ============================================================================= # =============================================================================
+46 -9
View File
@@ -12,7 +12,7 @@ from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select from sqlalchemy import select
from roboco.api.deps import CurrentAgentId, DbSession from roboco.api.deps import CurrentAgentContext, CurrentAgentId, DbSession
from roboco.api.schemas.notifications import ( from roboco.api.schemas.notifications import (
ListNotificationsParams, ListNotificationsParams,
NotificationCreateRequest, NotificationCreateRequest,
@@ -46,19 +46,56 @@ router = APIRouter()
) )
async def list_notifications( async def list_notifications(
db: DbSession, db: DbSession,
agent_id: CurrentAgentId, agent: CurrentAgentContext,
params: Annotated[ListNotificationsParams, Depends()], params: Annotated[ListNotificationsParams, Depends()],
) -> NotificationListResponse: ) -> NotificationListResponse:
"""List notifications for the agent.""" """List notifications for the agent.
query = build_notification_query(NotificationTable, agent_id, params)
result: Any = await db.execute(query)
notifications = result.scalars().all()
unread_count = sum(1 for n in notifications if agent_id not in n.read_by) System role (orchestrator) can see ALL notifications by type.
Regular agents only see notifications where they are a target.
"""
agent_id = agent.agent_id
# System role (orchestrator) bypasses to_agents filter
# This allows the dispatcher to query all pending escalations/a2a/etc.
if agent.role and agent.role.value == "system":
# Build query without to_agents filter
query = select(NotificationTable)
if params.pending_ack_only:
# For system, "pending" means has at least one target not yet acked
# We can't easily express "not fully acked" in SQL, so we filter
# after fetching. But we can at least require requires_ack=True.
query = query.where(NotificationTable.requires_ack.is_(True))
if params.type_filter:
query = query.where(NotificationTable.type == params.type_filter)
query = query.order_by(NotificationTable.timestamp.desc()).limit(params.limit)
# Execute and filter out fully acknowledged
result_all: Any = await db.execute(query)
all_notifications = result_all.scalars().all()
if params.pending_ack_only:
# Filter to only notifications not fully acknowledged
notifications = [
n for n in all_notifications
if not all(t in n.acked_by for t in n.to_agents)
]
else:
notifications = list(all_notifications)
else:
# Normal query - filter by to_agents
query = build_notification_query(NotificationTable, agent_id, params)
result: Any = await db.execute(query)
notifications = list(result.scalars().all())
# For system role, use a dummy agent_id for response formatting
response_agent_id = agent_id
unread_count = sum(1 for n in notifications if response_agent_id not in n.read_by)
pending_ack_count = sum( pending_ack_count = sum(
1 for n in notifications if n.requires_ack and agent_id not in n.acked_by 1 for n in notifications
if n.requires_ack and response_agent_id not in n.acked_by
) )
items = [notification_to_response(n, agent_id) for n in notifications] items = [notification_to_response(n, response_agent_id) for n in notifications]
return NotificationListResponse( return NotificationListResponse(
items=items, items=items,
+72 -12
View File
@@ -82,6 +82,45 @@ AGENT_IMAGES: dict[str, str] = {
"auditor": "roboco-agent-pm", "auditor": "roboco-agent-pm",
} }
# Complete list of MCP tools that trigger traceability reminders
# These tools represent key decision points where agents should document their work
TRACEABILITY_TRIGGER_TOOLS: list[str] = [
# === Task Lifecycle (All Roles) ===
"mcp__roboco-task__roboco_task_claim",
"mcp__roboco-task__roboco_task_plan",
"mcp__roboco-task__roboco_task_start",
"mcp__roboco-task__roboco_task_progress",
"mcp__roboco-task__roboco_task_pause",
"mcp__roboco-task__roboco_task_block",
"mcp__roboco-task__roboco_task_unblock",
"mcp__roboco-task__roboco_task_escalate",
"mcp__roboco-task__roboco_task_escalate_to_ceo",
"mcp__roboco-task__roboco_task_substitute",
# === Developer Submission ===
"mcp__roboco-task__roboco_task_submit_verification",
"mcp__roboco-task__roboco_task_submit_qa",
"mcp__roboco-task__roboco_task_submit_pm_review",
# === QA Tools ===
"mcp__roboco-task__roboco_task_qa_pass",
"mcp__roboco-task__roboco_task_qa_fail",
# === Documenter Tools ===
"mcp__roboco-task__roboco_task_docs_complete",
# === PM Tools ===
"mcp__roboco-task__roboco_task_create",
"mcp__roboco-task__roboco_task_activate",
"mcp__roboco-task__roboco_task_complete",
"mcp__roboco-task__roboco_task_cancel",
# === Git Tools ===
"mcp__roboco-git__roboco_git_commit",
"mcp__roboco-git__roboco_git_push",
"mcp__roboco-git__roboco_git_create_pr",
"mcp__roboco-git__roboco_git_merge_pr",
# === A2A Tools ===
"mcp__roboco-a2a__roboco_agent_request",
# === KB Tools ===
"mcp__roboco-optimal__roboco_ask_mentor",
]
def get_agent_image(agent_id: str) -> str: def get_agent_image(agent_id: str) -> str:
"""Get the Docker image for an agent.""" """Get the Docker image for an agent."""
@@ -470,8 +509,18 @@ class AgentOrchestrator:
] ]
} }
], ],
# Check for incoming A2A messages after each tool use
"PostToolUse": [ "PostToolUse": [
# Traceability reminders (context-aware, runs on specific tools)
{
"matcher": "|".join(TRACEABILITY_TRIGGER_TOOLS),
"hooks": [
{
"type": "command",
"command": "/app/scripts/traceability-hook.sh",
}
],
},
# Check for incoming A2A messages after each tool use
{ {
"matcher": "*", "matcher": "*",
"hooks": [ "hooks": [
@@ -480,7 +529,7 @@ class AgentOrchestrator:
"command": "/app/scripts/a2a-check-hook.sh", "command": "/app/scripts/a2a-check-hook.sh",
} }
], ],
} },
], ],
}, },
} }
@@ -2622,12 +2671,15 @@ Begin with step 1: roboco_task_get("{task_id}")
Monitors: escalation notifications (unacknowledged) Monitors: escalation notifications (unacknowledged)
Spawns: be-pm, fe-pm, ux-pm, main-pm, product-owner, head-marketing Spawns: be-pm, fe-pm, ux-pm, main-pm, product-owner, head-marketing
""" """
notifications = await self._fetch_notifications(client, "escalation") notifications = await self._fetch_notifications(client, "blocker_escalation")
for notif in notifications: for notif in notifications:
targets = notif.get("to_agents", []) targets = notif.get("to_agents", [])
for agent_id in targets: for agent_id in targets:
# Resolve UUID to slug - to_agents contains UUIDs from database
agent_slug = self._resolve_agent_slug(str(agent_id))
valid_targets = [ valid_targets = [
"be-pm", "be-pm",
"fe-pm", "fe-pm",
@@ -2636,14 +2688,14 @@ Begin with step 1: roboco_task_get("{task_id}")
"product-owner", "product-owner",
"head-marketing", "head-marketing",
] ]
if agent_id not in valid_targets: if agent_slug not in valid_targets:
continue continue
if self._is_agent_active(agent_id): if self._is_agent_active(agent_slug):
continue continue
await self.spawn_agent( await self.spawn_agent(
agent_id=agent_id, agent_id=agent_slug,
initial_prompt=self._build_escalation_prompt(notif), initial_prompt=self._build_escalation_prompt(notif),
) )
break break
@@ -2661,14 +2713,17 @@ Begin with step 1: roboco_task_get("{task_id}")
targets = notif.get("to_agents", []) targets = notif.get("to_agents", [])
for agent_id in targets: for agent_id in targets:
if agent_id not in ["product-owner", "head-marketing", "main-pm"]: # Resolve UUID to slug - to_agents contains UUIDs from database
agent_slug = self._resolve_agent_slug(str(agent_id))
if agent_slug not in ["product-owner", "head-marketing", "main-pm"]:
continue continue
if self._is_agent_active(agent_id): if self._is_agent_active(agent_slug):
continue continue
await self.spawn_agent( await self.spawn_agent(
agent_id=agent_id, agent_id=agent_slug,
initial_prompt=self._build_approval_prompt(notif), initial_prompt=self._build_approval_prompt(notif),
) )
break break
@@ -2686,7 +2741,9 @@ Begin with step 1: roboco_task_get("{task_id}")
for alert in alerts: for alert in alerts:
targets = alert.get("to_agents", []) targets = alert.get("to_agents", [])
if "auditor" in targets and not self._is_agent_active("auditor"): # Resolve UUIDs to slugs and check if auditor is a target
target_slugs = [self._resolve_agent_slug(str(t)) for t in targets]
if "auditor" in target_slugs and not self._is_agent_active("auditor"):
await self.spawn_agent( await self.spawn_agent(
agent_id="auditor", agent_id="auditor",
initial_prompt=self._build_audit_prompt(alert), initial_prompt=self._build_audit_prompt(alert),
@@ -2804,14 +2861,17 @@ Begin with step 1: roboco_task_get("{task_id}")
targets = notif.get("to_agents", []) targets = notif.get("to_agents", [])
for agent_id in targets: for agent_id in targets:
if self._is_agent_active(agent_id): # Resolve UUID to slug - to_agents contains UUIDs from database
agent_slug = self._resolve_agent_slug(str(agent_id))
if self._is_agent_active(agent_slug):
# Agent is online - SDK handles A2A delivery directly # Agent is online - SDK handles A2A delivery directly
# No action needed here, SDK server receives messages # No action needed here, SDK server receives messages
continue continue
# Agent is offline - spawn them with A2A context # Agent is offline - spawn them with A2A context
await self.spawn_agent( await self.spawn_agent(
agent_id=agent_id, agent_id=agent_slug,
initial_prompt=self._build_a2a_prompt(notif), initial_prompt=self._build_a2a_prompt(notif),
) )
break break
+9 -1
View File
@@ -370,6 +370,9 @@ class GitService(BaseService):
f"The parent task must be claimed first (claim creates branch)." f"The parent task must be claimed first (claim creates branch)."
) )
# Fetch to ensure we have the latest refs (critical for parent branches)
await self._run_git(workspace, ["fetch", "origin"])
# Create and push branch # Create and push branch
await self._run_git(workspace, ["checkout", base_branch]) await self._run_git(workspace, ["checkout", base_branch])
await self._run_git(workspace, ["pull", "origin", base_branch]) await self._run_git(workspace, ["pull", "origin", base_branch])
@@ -382,7 +385,12 @@ class GitService(BaseService):
return branch_name, base_branch return branch_name, base_branch
async def checkout(self, workspace: Path, branch: str) -> None: async def checkout(self, workspace: Path, branch: str) -> None:
"""Checkout a branch.""" """Checkout a branch.
Fetches from origin first to ensure remote branches are available.
"""
# Fetch to ensure we have the latest refs
await self._run_git(workspace, ["fetch", "origin"])
await self._run_git(workspace, ["checkout", branch]) await self._run_git(workspace, ["checkout", branch])
async def push(self, workspace: Path, force: bool = False) -> tuple[str, int]: async def push(self, workspace: Path, force: bool = False) -> tuple[str, int]:
+4 -2
View File
@@ -404,8 +404,10 @@ class TaskService(BaseService):
Branch naming (via build_branch_name): Branch naming (via build_branch_name):
- Root: feature/team/ROOT_ID - Root: feature/team/ROOT_ID
- Subtask: feature/team/ROOT_ID/SUB_ID - Subtask: feature/team/ROOT_ID--SUB_ID
- Sub-subtask: feature/team/ROOT_ID/SUB_ID/SUBSUB_ID - Sub-subtask: feature/team/ROOT_ID--SUB_ID--SUBSUB_ID
Uses '--' separator for task hierarchy to avoid git ref conflicts.
Parent branch resolution: Parent branch resolution:
- Subtask: uses parent task's branch_name - Subtask: uses parent task's branch_name
+9 -4
View File
@@ -2,7 +2,11 @@
Branch Name Builder. Branch Name Builder.
Generates git branch names following the format: Generates git branch names following the format:
{type}/{team}/{root_uuid}/{subtask_uuid}/{subsubtask_uuid} {type}/{team}/{root_uuid}--{subtask_uuid}--{subsubtask_uuid}
Uses '--' separator for task hierarchy to avoid git ref conflicts.
Git cannot have both 'foo' as a branch AND 'foo/bar' as another branch,
so we use '--' instead of '/' for the task hierarchy portion.
Max 3 levels deep (root subtask sub-subtask). Max 3 levels deep (root subtask sub-subtask).
""" """
@@ -35,7 +39,7 @@ async def build_branch_name(
task_service: TaskService instance for fetching task hierarchy task_service: TaskService instance for fetching task hierarchy
Returns: Returns:
Branch name in format: {type}/{team}/{root}/{sub}/{subsub} Branch name in format: {type}/{team}/{root}--{sub}--{subsub}
Raises: Raises:
BranchNameError: If branch_type invalid, task not found, or hierarchy too deep BranchNameError: If branch_type invalid, task not found, or hierarchy too deep
@@ -70,9 +74,10 @@ async def build_branch_name(
f"Task {task_id} has ancestors beyond the maximum depth." f"Task {task_id} has ancestors beyond the maximum depth."
) )
# Reverse to get root-first order: root/sub/subsub # Reverse to get root-first order: root--sub--subsub
# Use '--' separator to avoid git ref conflicts
ancestors.reverse() ancestors.reverse()
path = "/".join(ancestors) path = "--".join(ancestors)
return f"{branch_type}/{team}/{path}" return f"{branch_type}/{team}/{path}"