mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Traceability hooks and other fixes
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user