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

7.9 KiB
Raw Blame History

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

"""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):

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:

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

Добавить (если ещё нет):

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

Проверка

# 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 что комментарий появился