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 names follow the pattern: `{type}/{team}/{task-id[:8]}`
Branch names follow the pattern: `{type}/{team}/{task-hierarchy}`
**Types:** `feature`, `bug`, `chore`, `docs`, `hotfix`
**Task Hierarchy:** Uses `--` separator (not `/`) to avoid git ref conflicts.
**Examples:**
- `feature/backend/ABC12345`
- `bug/frontend/DEF67890`
- `hotfix/backend/GHI11111`
- Root task: `feature/backend/ABC12345`
- Subtask: `feature/backend/ABC12345--DEF67890`
- Sub-subtask: `feature/backend/ABC12345--DEF67890--GHI11111`
### 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
3. **Message when starting** - Announce to cell
4. **Read dev's journey** - `roboco_journal_read_team()` required
5. **Reflect before submit** - `roboco_journal_reflect()` required
6. **Use roboco_docs_write** - System handles paths and deduplication
7. **Quality docs** - Future developers depend on this
8. **Cannot complete** - Only PM completes after review
5. **Journal as you go** - Decisions, learnings, struggles
6. **Reflect before submit** - `roboco_journal_reflect()` REQUIRED
7. **Use roboco_docs_write** - System handles paths and deduplication
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
+44 -12
View File
@@ -18,19 +18,40 @@ For communication structure: `roboco_kb_search("communication hierarchy")`
## 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
Use `roboco_task_scan()` for tasks assigned to you from Board/CEO.
### 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.
### 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.
@@ -58,16 +79,16 @@ roboco_task_create(
- 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.
### 6. PAUSE + IDLE
### 7. PAUSE + 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.
### 8. REVIEW PR (Git Tasks)
### 9. REVIEW PR (Git Tasks)
When cell tasks reach `awaiting_pm_review` and all subtasks are merged:
1. Review the parent PR (all subtask work combined)
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.**
### 9. COMPLETE
### 10. COMPLETE
When CEO approves and PR is merged: reflect + complete your task.
## Your Tools
@@ -108,6 +129,11 @@ When CEO approves and PR is merged: reflect + complete your task.
**Group Management (Main PM ONLY):**
- `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:**
- `roboco_message_send`, `roboco_channel_history`, `roboco_channel_list`
- `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
- `roboco_session_create_for_tasks` → Cell PM creates sessions
- `roboco_task_submit_qa` → Developer only
- `roboco_task_qa_pass`, `roboco_task_qa_fail` → QA 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
4. **Pause after distributing** - Don't spin waiting
5. **Monitor periodically** - Check progress, unblock if needed
6. **Journal your decisions** - Why this breakdown?
7. **Complete when ALL cell tasks done** - Verify before completing
6. **Journal as you go** - Decisions, learnings, struggles
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
+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
3. **Message when starting** - Announce to cell
4. **Read dev's journey** - `roboco_journal_read_team()` required
5. **Journal your review** - Document what was tested
6. **Reflect before decision** - `roboco_journal_reflect()` required
5. **Journal as you go** - Decisions, learnings, struggles
6. **Reflect before decision** - `roboco_journal_reflect()` REQUIRED
7. **Clear fail reasons** - Developer needs to know what to fix
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
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
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/traceability-hook.sh /app/scripts/traceability-hook.sh
RUN chmod +x /app/scripts/*.sh
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 uvicorn
from fastapi import FastAPI
from fastapi import status as http_status
from roboco.agent_sdk.models import (
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
# =============================================================================
+44 -7
View File
@@ -12,7 +12,7 @@ from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
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 (
ListNotificationsParams,
NotificationCreateRequest,
@@ -46,19 +46,56 @@ router = APIRouter()
)
async def list_notifications(
db: DbSession,
agent_id: CurrentAgentId,
agent: CurrentAgentContext,
params: Annotated[ListNotificationsParams, Depends()],
) -> NotificationListResponse:
"""List notifications for the agent."""
"""List notifications for the agent.
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 = result.scalars().all()
notifications = list(result.scalars().all())
unread_count = sum(1 for n in notifications if agent_id not in n.read_by)
# 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(
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(
items=items,
+72 -12
View File
@@ -82,6 +82,45 @@ AGENT_IMAGES: dict[str, str] = {
"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:
"""Get the Docker image for an agent."""
@@ -470,8 +509,18 @@ class AgentOrchestrator:
]
}
],
# Check for incoming A2A messages after each tool use
"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": "*",
"hooks": [
@@ -480,7 +529,7 @@ class AgentOrchestrator:
"command": "/app/scripts/a2a-check-hook.sh",
}
],
}
},
],
},
}
@@ -2622,12 +2671,15 @@ Begin with step 1: roboco_task_get("{task_id}")
Monitors: escalation notifications (unacknowledged)
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:
targets = notif.get("to_agents", [])
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 = [
"be-pm",
"fe-pm",
@@ -2636,14 +2688,14 @@ Begin with step 1: roboco_task_get("{task_id}")
"product-owner",
"head-marketing",
]
if agent_id not in valid_targets:
if agent_slug not in valid_targets:
continue
if self._is_agent_active(agent_id):
if self._is_agent_active(agent_slug):
continue
await self.spawn_agent(
agent_id=agent_id,
agent_id=agent_slug,
initial_prompt=self._build_escalation_prompt(notif),
)
break
@@ -2661,14 +2713,17 @@ Begin with step 1: roboco_task_get("{task_id}")
targets = notif.get("to_agents", [])
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
if self._is_agent_active(agent_id):
if self._is_agent_active(agent_slug):
continue
await self.spawn_agent(
agent_id=agent_id,
agent_id=agent_slug,
initial_prompt=self._build_approval_prompt(notif),
)
break
@@ -2686,7 +2741,9 @@ Begin with step 1: roboco_task_get("{task_id}")
for alert in alerts:
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(
agent_id="auditor",
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", [])
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
# No action needed here, SDK server receives messages
continue
# Agent is offline - spawn them with A2A context
await self.spawn_agent(
agent_id=agent_id,
agent_id=agent_slug,
initial_prompt=self._build_a2a_prompt(notif),
)
break
+9 -1
View File
@@ -370,6 +370,9 @@ class GitService(BaseService):
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
await self._run_git(workspace, ["checkout", base_branch])
await self._run_git(workspace, ["pull", "origin", base_branch])
@@ -382,7 +385,12 @@ class GitService(BaseService):
return branch_name, base_branch
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])
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):
- Root: feature/team/ROOT_ID
- Subtask: feature/team/ROOT_ID/SUB_ID
- Sub-subtask: feature/team/ROOT_ID/SUB_ID/SUBSUB_ID
- Subtask: feature/team/ROOT_ID--SUB_ID
- Sub-subtask: feature/team/ROOT_ID--SUB_ID--SUBSUB_ID
Uses '--' separator for task hierarchy to avoid git ref conflicts.
Parent branch resolution:
- Subtask: uses parent task's branch_name
+9 -4
View File
@@ -2,7 +2,11 @@
Branch Name Builder.
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).
"""
@@ -35,7 +39,7 @@ async def build_branch_name(
task_service: TaskService instance for fetching task hierarchy
Returns:
Branch name in format: {type}/{team}/{root}/{sub}/{subsub}
Branch name in format: {type}/{team}/{root}--{sub}--{subsub}
Raises:
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."
)
# 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()
path = "/".join(ancestors)
path = "--".join(ancestors)
return f"{branch_type}/{team}/{path}"