Files
wiki/tasks/multi-agent/DEV_TASK_PLANE_SYNC.md
2026-05-21 21:10:01 +03:00

232 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DEV_TASK_PLANE_SYNC.md — Обратная связь Orchestrator → Plane
---
type: dev-task
priority: high
project: multi-agent
created_at: 2026-05-21
author: stream
---
## Контекст
Orchestrator получает webhooks из Plane, но не пишет обратно. Слава не видит прогресс задач в Plane. Нужно добавить sync: при смене stage → обновлять state issue, добавлять комментарии.
## Plane API
**Base URL:** `http://localhost:8091/api/v1`
**Auth:** Header `X-API-Key: <ORCH_PLANE_API_TOKEN>` (уже в .env)
**Workspace:** `ag_proj`
**Project ID:** `7a79f0a9-5278-49cd-9007-9a338f238f9c`
### States (project "Enduro Trails")
| ID | Name | Group |
|----|------|-------|
| `113b24f6-cce8-4be9-9a22-a359b9cf0122` | Backlog | backlog |
| `2c7d3df3-9eb9-419b-92b7-d7d560bcdd10` | Todo | unstarted |
| `b873d9eb-993c-48cd-97ac-99a9b1623967` | In Progress | started |
| `381a2833-3c4e-4be5-bd0f-be84cb946ad8` | Done | completed |
| `b1cae7f9-961d-4889-a179-f3acea697d17` | Cancelled | cancelled |
### API Endpoints
**Update issue state:**
```
PATCH /workspaces/ag_proj/projects/{project_id}/issues/{issue_id}/
Body: {"state": "<state_id>"}
```
**Add comment:**
```
POST /workspaces/ag_proj/projects/{project_id}/issues/{issue_id}/comments/
Body: {"comment_html": "<p>text</p>"}
```
**Get issue by identifier (sequence_id):**
```
GET /workspaces/ag_proj/projects/{project_id}/issues/?search=ET-002
```
## Задачи
### 1. Создать модуль `src/plane_sync.py`
```python
"""Plane API sync — update issue state and add comments."""
import logging
import httpx
from .config import settings
logger = logging.getLogger("orchestrator.plane_sync")
PLANE_BASE = f"{settings.plane_api_url}/api/v1"
PLANE_HEADERS = {"X-API-Key": settings.plane_api_token}
WORKSPACE = settings.plane_workspace_slug
PROJECT_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c"
# Map orchestrator stages to Plane states
STAGE_TO_STATE = {
"created": "2c7d3df3-9eb9-419b-92b7-d7d560bcdd10", # Todo
"analysis": "b873d9eb-993c-48cd-97ac-99a9b1623967", # In Progress
"architecture": "b873d9eb-993c-48cd-97ac-99a9b1623967", # In Progress
"development": "b873d9eb-993c-48cd-97ac-99a9b1623967", # In Progress
"review": "b873d9eb-993c-48cd-97ac-99a9b1623967", # In Progress
"testing": "b873d9eb-993c-48cd-97ac-99a9b1623967", # In Progress
"deploy": "b873d9eb-993c-48cd-97ac-99a9b1623967", # In Progress
"done": "381a2833-3c4e-4be5-bd0f-be84cb946ad8", # Done
}
def find_issue_id(work_item_id: str) -> str | None:
"""Find Plane issue UUID by work_item_id (e.g. 'ET-002')."""
url = f"{PLANE_BASE}/workspaces/{WORKSPACE}/projects/{PROJECT_ID}/issues/"
try:
resp = httpx.get(url, headers=PLANE_HEADERS, params={"search": work_item_id}, timeout=10)
resp.raise_for_status()
data = resp.json()
results = data.get("results", data if isinstance(data, list) else [])
for issue in results:
# Match by sequence_id or name containing work_item_id
seq = issue.get("sequence_id")
identifier = f"ET-{seq}" if seq else ""
if identifier == work_item_id or work_item_id in issue.get("name", ""):
return issue["id"]
except Exception as e:
logger.error(f"Failed to find issue for {work_item_id}: {e}")
return None
def update_issue_state(work_item_id: str, stage: str):
"""Update Plane issue state based on orchestrator stage."""
state_id = STAGE_TO_STATE.get(stage)
if not state_id:
return
issue_id = find_issue_id(work_item_id)
if not issue_id:
logger.warning(f"Issue not found in Plane for {work_item_id}")
return
url = f"{PLANE_BASE}/workspaces/{WORKSPACE}/projects/{PROJECT_ID}/issues/{issue_id}/"
try:
resp = httpx.patch(url, headers=PLANE_HEADERS, json={"state": state_id}, timeout=10)
resp.raise_for_status()
logger.info(f"Plane: {work_item_id} state → {stage} ({state_id[:8]}...)")
except Exception as e:
logger.error(f"Failed to update Plane state for {work_item_id}: {e}")
def add_comment(work_item_id: str, text: str):
"""Add a comment to Plane issue."""
issue_id = find_issue_id(work_item_id)
if not issue_id:
logger.warning(f"Issue not found in Plane for {work_item_id}, skipping comment")
return
url = f"{PLANE_BASE}/workspaces/{WORKSPACE}/projects/{PROJECT_ID}/issues/{issue_id}/comments/"
html = f"<p>{text}</p>"
try:
resp = httpx.post(url, headers=PLANE_HEADERS, json={"comment_html": html}, timeout=10)
resp.raise_for_status()
logger.info(f"Plane: comment added to {work_item_id}")
except Exception as e:
logger.error(f"Failed to add comment to {work_item_id}: {e}")
def notify_stage_change(work_item_id: str, old_stage: str, new_stage: str, agent: str = None):
"""Notify Plane about stage transition."""
update_issue_state(work_item_id, new_stage)
msg = f"🔄 Stage: {old_stage}{new_stage}"
if agent:
msg += f" (launching {agent})"
add_comment(work_item_id, msg)
def notify_qg_failure(work_item_id: str, stage: str, check: str, reason: str):
"""Notify Plane about QG failure."""
add_comment(work_item_id, f"⚠️ QG failed at {stage}: {check}{reason}")
def notify_done(work_item_id: str):
"""Mark issue as Done in Plane."""
update_issue_state(work_item_id, "done")
add_comment(work_item_id, "✅ Task completed! PR merged and deployed.")
```
### 2. Интегрировать в webhooks/plane.py
В функции `_try_advance_stage`, после `update_task_stage(task_id, next_stage)`:
```python
from ..plane_sync import notify_stage_change as plane_notify_stage, notify_qg_failure as plane_notify_qg
# После успешного advance:
plane_notify_stage(work_item_id, current_stage, next_stage, agent)
# После QG failure:
plane_notify_qg(work_item_id, current_stage, qg_name, reason)
```
### 3. Интегрировать в webhooks/gitea.py
В `handle_ci_status` и `handle_pr_review`, после advance:
```python
from ..plane_sync import notify_stage_change as plane_notify_stage
# После advance:
plane_notify_stage(work_item_id, current_stage, next_stage, agent)
```
### 4. Добавить config fields
**Файл:** `/home/slin/repos/orchestrator/src/config.py`
Добавить (если ещё нет):
```python
plane_api_url: str = os.getenv("ORCH_PLANE_API_URL", "http://localhost:8091")
plane_api_token: str = os.getenv("ORCH_PLANE_API_TOKEN", "")
plane_workspace_slug: str = os.getenv("ORCH_PLANE_WORKSPACE_SLUG", "ag_proj")
```
---
## Файлы для изменения
- `/home/slin/repos/orchestrator/src/plane_sync.py` (НОВЫЙ)
- `/home/slin/repos/orchestrator/src/webhooks/plane.py` (добавить вызовы)
- `/home/slin/repos/orchestrator/src/webhooks/gitea.py` (добавить вызовы)
- `/home/slin/repos/orchestrator/src/config.py` (проверить что plane fields есть)
## Ограничения
- НЕ менять порт 8500
- НЕ менять формат .env (все переменные уже есть)
- НЕ блокировать основной flow если Plane API недоступен (try/except, log error, continue)
- httpx уже в requirements.txt
## Проверка
```bash
# 1. Rebuild
cd /home/slin/repos/orchestrator && docker compose up -d --build
# 2. Health
curl -s http://localhost:8500/health
# 3. Smoke test — вызвать plane_sync напрямую
docker exec orchestrator python -c "
from src.plane_sync import find_issue_id, add_comment
issue_id = find_issue_id('ET-002')
print(f'Found issue: {issue_id}')
if issue_id:
add_comment('ET-002', '🧪 Test comment from Orchestrator')
print('Comment added!')
"
# 4. Проверить в Plane UI что комментарий появился
```