feat(plane): unified status-comment format with duration line (ORCH-016) (#34)
This commit was merged in pull request #34.
This commit is contained in:
7
docs/work-items/ORCH-016/00-business-request.md
Normal file
7
docs/work-items/ORCH-016/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: Единообразные коммент-артефакты в Plane от всех агентов
|
||||
|
||||
Work Item ID: ORCH-016
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
85
docs/work-items/ORCH-016/01-brd.md
Normal file
85
docs/work-items/ORCH-016/01-brd.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# BRD: Единообразные коммент-артефакты в Plane от всех агентов
|
||||
|
||||
Work Item ID: **ORCH-016**
|
||||
Стадия: analysis
|
||||
Автор: analyst
|
||||
Дата: 2026-06-05
|
||||
Ревизия: 2 (учтён фидбэк стейкхолдера от 2026-06-05 — добавить длительность работы агента в коммент)
|
||||
|
||||
---
|
||||
|
||||
## 1. Бизнес-цель
|
||||
Стейкхолдер (Слава) должен мочь из ленты комментариев задачи в Plane **за один клик** перейти к артефакту любого агента (ADR, PR, ревью, отчёт тестера, деплой-лог), а не разбирать «шумные» строки без удобной ссылки и человекочитаемого описания.
|
||||
Помимо ссылок, по комментариям стейкхолдер хочет **видеть, сколько работал каждый агент** (длительность стадии), не открывая БД оркестратора и не лезя в `agent_runs`.
|
||||
|
||||
## 2. Мотивация
|
||||
Сейчас в Plane комменты двух разных стилей:
|
||||
|
||||
| Кто пишет | Формат коммента | Источник |
|
||||
|-----------|-----------------|----------|
|
||||
| **Аналитик (эталон)** | HTML: человеческое описание стадии + `<ul>` со списком ссылок на артефакты, заголовок «Документы:» | `src/stage_engine.py::_build_analyst_ready_comment` (PR #13) |
|
||||
| Architect / Developer / Reviewer / Tester / Deployer | Однострочник «{icon} Role готов · 8.5M in / 45.8k out · $7.29» + markdown-ссылки следом | `src/usage.py::usage_comment` + `artifact_links` |
|
||||
|
||||
Проблемы второго формата:
|
||||
1. Нет человеческого описания результата стадии — есть только техническая метрика «tokens/cost».
|
||||
2. Нет краткого вердикта одной строкой там, где он есть в артефакте (Reviewer `APPROVE/REQUEST_CHANGES`, Tester `PASS/FAIL`, Deployer `SUCCESS/FAILED`).
|
||||
3. Формат разнится по агентам (где-то «📂 Branch + 🔗 PR», где-то «📄 Test report») — нет единого визуального якоря.
|
||||
4. **Не видно длительности стадии** — стейкхолдер не понимает, агент отработал за 30 секунд или за 12 минут; это важная метрика для оценки SLA, поведения долгих стадий (testing/deploy) и подозрений на «зависание».
|
||||
|
||||
## 3. Целевая аудитория
|
||||
- **Стейкхолдер задачи (Слава, владелец продукта)** — главный потребитель ленты комментариев в Plane.
|
||||
- **Reviewer / QA / DevOps по другим проектам (enduro-trails)** — те же ссылки помогут им навигироваться по задачам, не открывая БД оркестратора.
|
||||
|
||||
## 4. Scope (что входит)
|
||||
1. Привести коммент-формат **architect, developer, reviewer, tester, deployer** к единому виду по эталону аналитика:
|
||||
- заголовок-роль (emoji + имя роли),
|
||||
- короткое человеческое описание результата стадии (1 предложение),
|
||||
- кликабельная ссылка(и) на СВОЙ артефакт,
|
||||
- **одна строка-вердикт** там, где это уместно (Reviewer / Tester / Deployer),
|
||||
- **одна строка-длительность** работы агента — для всех ролей, включая аналитика.
|
||||
2. Переиспользовать `settings.gitea_public_url` для кликабельных ссылок (готово в PR #14).
|
||||
3. Сохранить существующее поведение аналитика (PR #13) — он уже соответствует целевому формату; в идеале — переиспользовать общий хелпер. К аналитику также добавляется строка длительности.
|
||||
4. Один коммент на агента за прохождение стадии (без спама).
|
||||
5. Источник длительности — уже существующая метрика `_duration_s` в `src/agents/launcher.py` (или `agent_runs.started_at` / `finished_at`). Новых таблиц/полей в БД не заводим.
|
||||
|
||||
## 5. Out of scope (что НЕ трогаем)
|
||||
- Логика Quality Gates (`src/qg/checks.py`).
|
||||
- Status-only verdict model (PR #12) — приёмка аналитика через смену статуса Plane на «Approved/Rejected».
|
||||
- Дедупликация вебхуков (`src/webhooks/_dedup.py`).
|
||||
- `set_issue_done`, `notify_done`, `notify_qg_failure` — внутренние нотификации остаются как есть.
|
||||
- Per-agent bot-авторство (PR с `PLANE_BOT_TOKENS`) — сохраняется.
|
||||
- Изменение схемы БД, конвейера стадий, реестра QG.
|
||||
|
||||
## 6. Бизнес-требования
|
||||
**BR-1.** Каждый агент по завершении своей стадии (вне пути ошибки) пишет в Plane **ровно один** коммент в едином формате.
|
||||
**BR-2.** Коммент содержит:
|
||||
- заголовок с emoji-иконкой роли и человекочитаемым названием,
|
||||
- 1–2 предложения с описанием результата стадии на русском языке,
|
||||
- кликабельную ссылку (-и) на артефакт(ы) этого агента в Gitea,
|
||||
- одну строку вердикта (Verdict / Status), если артефакт его содержит,
|
||||
- **одну строку длительности работы агента** (`Длительность: <human-format>`), всегда, если значение известно.
|
||||
**BR-3.** Ссылки строятся через `gitea_public_url` (fallback на `gitea_url`).
|
||||
**BR-4.** Формат должен быть устойчив: отсутствующий артефакт / отсутствующий вердикт / неизвестная длительность не ломают коммент — соответствующая строка просто опускается.
|
||||
**BR-5.** Изменение **не нарушает**:
|
||||
- status-only verdict model (аналитик по-прежнему ждёт смены статуса Plane),
|
||||
- дедуп комментов и вебхуков,
|
||||
- работу `set_issue_done` / `notify_done` на финале конвейера,
|
||||
- per-agent bot-авторство.
|
||||
**BR-6.** Длительность отображается в человекочитаемой форме (`12s`, `4m 12s`, `1h 03m`), а не в виде голых секунд. Источник — `agent_runs.started_at` / `finished_at` (или уже посчитанный `_duration_s` в `launcher.py`). Новых полей в БД не вводится.
|
||||
|
||||
## 7. Ограничения и риски
|
||||
- **Self-hosting:** оркестратор правит сам себя; деплой только через staging-гейт (порт 8501) → прод-контейнер `orchestrator` не перезапускать в рамках задачи.
|
||||
- Прод обслуживает другие проекты (enduro-trails) — нельзя сломать комменты в их задачах.
|
||||
- Plane Bot-авторство (`_headers_for`) должно остаться — коммент пишется под бот-токеном своей роли.
|
||||
- Reviewer/tester вердикты читаются из артефактов; нужно идемпотентно работать, если артефакт ещё не закоммичен / не доступен в worktree.
|
||||
|
||||
## 8. Связки
|
||||
- PR #13 — `status-only analyst comment with doc links` (эталон формата аналитика).
|
||||
- PR #14 — `external gitea_public_url for clickable doc links` (источник кликабельных ссылок).
|
||||
- ADR не требуется: сквозной архитектурный сдвиг отсутствует, меняем только формирование текста коммента в существующем потоке.
|
||||
|
||||
## 9. Критерии успеха (high-level)
|
||||
- Слава открывает любую задачу в Plane и в ленте видит однотипные карточки от каждого агента: «{role} — {описание} → ссылка [Verdict: …] [Длительность: …]».
|
||||
- По любой ссылке открывается соответствующий документ в Gitea (HTTP 200, корректный путь).
|
||||
- В каждом статус-комменте присутствует строка «Длительность: …» с человекочитаемым значением (`12s` / `4m 12s` / `1h 03m`).
|
||||
- Никаких регрессий в существующих тестах `tests/`.
|
||||
174
docs/work-items/ORCH-016/02-trz.md
Normal file
174
docs/work-items/ORCH-016/02-trz.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# ТЗ: Единообразные коммент-артефакты в Plane от всех агентов
|
||||
|
||||
Work Item ID: **ORCH-016**
|
||||
Стадия: analysis → architecture → development
|
||||
Автор: analyst
|
||||
Дата: 2026-06-05
|
||||
Ревизия: 2 (по фидбэку стейкхолдера — добавлен §2.5 Duration; обновлены §1, §2.1, §6)
|
||||
|
||||
> Контракт: что именно меняем в коде / какие модули задействованы / какие проверки появятся.
|
||||
> Архитектурные решения принимает архитектор; здесь — границы изменения.
|
||||
|
||||
---
|
||||
|
||||
## 1. Задействованные модули
|
||||
|
||||
| Модуль | Роль в изменении |
|
||||
|--------|------------------|
|
||||
| `src/usage.py` | **Главная точка изменения.** Здесь сейчас живут `usage_comment()`, `artifact_links()`, `AGENT_ARTIFACT`, `AGENT_DISPLAY`, `AGENT_ICON` — основа форматирования. Нужно расширить/добавить хелпер построения единого status-коммента + утилитку форматирования длительности (`fmt_duration(seconds: int) -> str`). |
|
||||
| `src/stage_engine.py` | Эталонная функция аналитика `_build_analyst_ready_comment()`. По возможности — переиспользовать новый общий хелпер (или хотя бы выровнять формат: emoji + заголовок + описание + список ссылок). К аналитику также прикручиваем строку длительности (см. §2.5). |
|
||||
| `src/agents/launcher.py` | `_post_usage_comments()` — точка, где постится коммент по завершении агента (architect/developer/reviewer/tester/deployer). Должен звать новый хелпер. `_duration_s` уже считается на строке `391` — пробросить его (или достать из `agent_runs.started_at`/`finished_at`) в хелпер. |
|
||||
| `src/db.py` | **Только для чтения** в рантайме коммент-хелпера: `agent_runs.started_at`, `agent_runs.finished_at` (уже существуют). Никаких ALTER. |
|
||||
| `src/plane_sync.py` | `add_comment()` — без изменений (используется как транспорт). |
|
||||
| `src/qg/checks.py` | **Только для чтения**: модели парсинга frontmatter `verdict:` / `deploy_status:` / `staging_status:` — переиспользуем эту логику (вынести в отдельную утилитку, либо импортировать там, где она уже есть). |
|
||||
| `src/config.py` | `settings.gitea_public_url`, `settings.gitea_owner`, `settings.gitea_url` — без изменений, переиспользуются. |
|
||||
|
||||
## 2. Контракт нового коммент-формата
|
||||
|
||||
### 2.1 Структура (одинакова для всех агентов)
|
||||
```
|
||||
{ICON} {RoleName} — {one-line human description of stage result}
|
||||
|
||||
[Verdict / Status: <VALUE>] # опционально, см. 2.3
|
||||
Длительность: <human-format> # см. 2.5; опускается, только если значение неизвестно
|
||||
<b>Документы:</b>
|
||||
• <a href="…">{label}</a> # одна или несколько ссылок
|
||||
```
|
||||
|
||||
Поля:
|
||||
- `{ICON}` — берётся из `AGENT_ICON` (уже есть в `usage.py`).
|
||||
- `{RoleName}` — из `AGENT_DISPLAY` (уже есть).
|
||||
- `{description}` — фиксированная строка на роль, см. 2.2.
|
||||
- Verdict / Status — см. 2.3, опускается если не извлекается.
|
||||
- Длительность — см. 2.5, печатается всегда, когда значение есть; по умолчанию доступна (это нативная метрика `agent_runs`).
|
||||
- Ссылки — см. 2.4.
|
||||
|
||||
### 2.2 Описания стадий (per-agent text)
|
||||
|
||||
| Агент | Описание (рус.) |
|
||||
|-------|------------------|
|
||||
| analyst | «Подготовил BRD / ТЗ / Acceptance Criteria. Для продвижения переведите задачу в статус Approved.» (как сейчас в `_build_analyst_ready_comment`) |
|
||||
| architect | «Завершил архитектурную проработку. См. ADR ниже.» |
|
||||
| developer | «Завершил разработку. См. PR / branch ниже.» |
|
||||
| reviewer | «Завершил ревью изменений.» |
|
||||
| tester | «Завершил прогон тестов.» |
|
||||
| deployer | «Завершил деплой.» |
|
||||
|
||||
Точные формулировки финализирует architect; аналитик фиксирует **факт** наличия 1-предложного описания на каждую роль.
|
||||
|
||||
### 2.3 Verdict / Status строка
|
||||
|
||||
Печатается отдельной строкой над списком документов. Источник — frontmatter артефакта; парсить идемпотентно (если файл недоступен — строку пропустить):
|
||||
|
||||
| Агент | Поле | Где парсим | Возможные значения | Формат строки |
|
||||
|-------|------|------------|---------------------|----------------|
|
||||
| analyst | — | — | — | не печатается |
|
||||
| architect | — | — | — | не печатается |
|
||||
| developer | — | — | — | не печатается (CI-статус — отдельный гейт) |
|
||||
| reviewer | `verdict:` | `docs/work-items/<wid>/12-review.md` (YAML-frontmatter) | `APPROVE` / `REQUEST_CHANGES` | `Verdict: APPROVE` |
|
||||
| tester | `verdict:` (или эквивалентный фронт-кей) | `docs/work-items/<wid>/13-test-report.md` | `PASS` / `FAIL` | `Verdict: PASS` |
|
||||
| deployer | `staging_status:` (для deploy-staging) / `deploy_status:` (для deploy) | `15-staging-log.md` / `14-deploy-log.md` | `SUCCESS` / `FAILED` | `Status: SUCCESS` |
|
||||
|
||||
Если значение в frontmatter отсутствует или не распознано → строка `Verdict / Status` НЕ выводится (вердикт-парсинг гейтов и сама логика гейтов не меняется).
|
||||
|
||||
### 2.4 Ссылки на артефакты
|
||||
|
||||
Базовый URL: `(settings.gitea_public_url or settings.gitea_url).rstrip('/')`.
|
||||
Префикс: `/{owner}/{repo}/src/branch/{branch}/`.
|
||||
|
||||
| Агент | Артефакты (label → путь) |
|
||||
|-------|----------------------------|
|
||||
| analyst | BRD `01-brd.md`, ТЗ `02-trz.md`, AC `03-acceptance-criteria.md`, Test Plan `04-test-plan.yaml` *(уже есть)* |
|
||||
| architect | ADR-папка `docs/work-items/<wid>/06-adr/` *(уже есть)* |
|
||||
| developer | Branch `…/src/branch/<branch>`, PR `…/pulls/<num>` *(уже есть)* |
|
||||
| reviewer | Review `docs/work-items/<wid>/12-review.md` *(уже есть)* |
|
||||
| tester | Test report `docs/work-items/<wid>/13-test-report.md` *(уже есть)* |
|
||||
| deployer | Deploy log `docs/work-items/<wid>/14-deploy-log.md`; staging-лог `15-staging-log.md` (если применимо к стадии) |
|
||||
|
||||
Несуществующий файл в worktree → ссылка опускается (как сейчас в `_build_analyst_ready_comment`).
|
||||
|
||||
### 2.5 Строка длительности работы агента
|
||||
|
||||
**Что печатаем:** одну строку вида `Длительность: {human}` (или `Duration: {human}` — финальную локализацию метки фиксирует архитектор; русский предпочтителен, остальные комменты уже на русском).
|
||||
|
||||
**Источник значения (приоритет сверху вниз):**
|
||||
|
||||
1. **Параметр функции** — `_post_usage_comments()` в `src/agents/launcher.py:682` вызывается из контекста, где `_duration_s` уже посчитан на строке `391` (`int(time.time() - _start_ts)`). Простейший путь — пробросить `duration_s` явным аргументом в `usage_comment(...)` / новый `build_status_comment(...)`.
|
||||
2. **Fallback из БД** — если параметр не передан (например, для аналитика, чей коммент строится в `_build_analyst_ready_comment` в `src/stage_engine.py:298`), читаем
|
||||
```sql
|
||||
SELECT
|
||||
CAST((julianday(finished_at) - julianday(started_at)) * 86400 AS INTEGER)
|
||||
FROM agent_runs
|
||||
WHERE task_id = ? AND agent = ?
|
||||
ORDER BY id DESC LIMIT 1
|
||||
```
|
||||
Это последний завершённый run этой роли по задаче.
|
||||
3. **Если оба источника пусты / `None` / отрицательны** — строка `Длительность:` НЕ печатается (graceful, как и для вердикта).
|
||||
|
||||
**Форматирование (`fmt_duration(seconds: int) -> str` в `src/usage.py`):**
|
||||
|
||||
| Диапазон | Формат | Пример |
|
||||
|----------|--------|--------|
|
||||
| `0 ≤ s < 60` | `{s}s` | `12s`, `45s` |
|
||||
| `60 ≤ s < 3600` | `{m}m {ss}s` | `4m 12s`, `1m 03s` |
|
||||
| `s ≥ 3600` | `{h}h {mm}m` (секунды отбрасываем) | `1h 03m`, `2h 47m` |
|
||||
|
||||
Округление: целые секунды (input — `int`). При `s == 0` всё равно печатаем `0s` (видно, что метрика известна и стадия отработала почти мгновенно).
|
||||
|
||||
**Покрытие ролей:** строка длительности добавляется для **всех** агентов, включая аналитика. Для аналитика — строго через fallback из `agent_runs` (его коммент строится в `stage_engine.py`, не в `launcher.py`).
|
||||
|
||||
**Что НЕ делаем:**
|
||||
- Не меняем схему `agent_runs` (поля `started_at` / `finished_at` уже есть, `_duration_s` уже считается).
|
||||
- Не изобретаем новый отдельный коммент с длительностью — длительность встраивается в существующий status-коммент.
|
||||
- Не считаем «время от первого вебхука до коммента» — берём чистое время процесса агента (тот же `_duration_s`, что попадает в `notify_agent_finished`), чтобы значение совпадало с тем, что уже видно в Telegram live tracker / логах.
|
||||
|
||||
### 2.6 Один коммент на агента за стадию
|
||||
Текущий триггер — `_post_usage_comments()` вызывается **один раз** в успешном auto-advance пути после агента. Никаких новых триггеров не добавляем. Дубликаты исключены текущей логикой (одно завершение агента → один коммент).
|
||||
|
||||
### 2.7 Usage-метрики (токены / стоимость)
|
||||
Текущий `usage_comment()` встраивает «8.5M in / 45.8k out · $7.29» в первый строкой. По требованиям Славы это «без раздувания», но не запрещено явно. Решение:
|
||||
- **Сохранить** usage-метрику как **последнюю строку** коммента (мелким техническим хвостом, например `<sub>8.5M in / 45.8k out · $7.29 · Длительность: 4m 12s</sub>`), либо
|
||||
- **Перенести** в `task_summary_comment` (только для финального deployer-summary).
|
||||
|
||||
Финальный выбор — за архитектором (см. вопрос Q-1 в `10-tech-risks.md`). Длительность из §2.5 — **отдельная** строка от usage-метрики и присутствует независимо от того, как решится вопрос про токены/стоимость.
|
||||
|
||||
### 2.8 Бот-авторство
|
||||
`plane_add_comment(..., author=<role>)` — сохраняется. Все агенты комментируют под своим bot-токеном (`PLANE_BOT_TOKENS`). Изменения формата текста на это не влияют.
|
||||
|
||||
## 3. Изменения API
|
||||
**Нет.** Внешние webhooks (`/webhook/plane`, `/webhook/gitea`), `/health`, `/status`, `/queue` — не меняются.
|
||||
|
||||
## 4. Изменения схемы БД
|
||||
**Нет.** Используются существующие таблицы `tasks`, `agent_runs`, `jobs`.
|
||||
|
||||
## 5. Новые Quality Gate checks
|
||||
**Нет.** Гейты не меняются. Парсинг `verdict:` / `deploy_status:` / `staging_status:` в коммент — отдельная утилитка, не QG.
|
||||
|
||||
## 6. Требования к коду
|
||||
- Все новые функции — с docstring (зачем нужны, какие инварианты сохраняют).
|
||||
- Парсинг frontmatter артефакта — graceful: исключение → строка вердикта опускается, лог в `logger.debug`.
|
||||
- Чтение длительности — graceful: исключение или `None` → строка длительности опускается, лог в `logger.debug`. Отрицательные / нулевые значения: `0` печатается как `0s`, отрицательные опускаются.
|
||||
- `fmt_duration(seconds: int) -> str` — чистая, без БД-зависимостей, легко тестируется юнитом.
|
||||
- Никаких новых внешних зависимостей: использовать `pyyaml` (уже в проекте) или существующий парсер frontmatter из `src/qg/checks.py`.
|
||||
- Поведение для проектов **без** артефактов (например, ENDURO-* до запуска агента) — graceful no-op: коммент с описанием и без ссылок (минимум — заголовок).
|
||||
- HTML (как у аналитика) предпочтительнее markdown — Plane корректно рендерит `<ul><li><a>` и `<b>`.
|
||||
|
||||
## 7. Артефакты по pipeline
|
||||
- `06-adr/` — **не требуется** (нет архитектурного сдвига; обсуждается локально архитектором, в случае спорного решения по 2.6 — заводим ADR `ADR-001-status-comment-format.md`).
|
||||
- `07-infra-requirements.md` — **не требуется** (нет новой инфраструктуры).
|
||||
- `08-data-requirements.md` — **не требуется** (БД не меняется).
|
||||
- `12-review.md` / `13-test-report.md` / `14-deploy-log.md` — формируются на соответствующих стадиях по канону.
|
||||
- `CHANGELOG.md` — обновить в том же PR (раздел `Unreleased`).
|
||||
|
||||
## 8. Документация
|
||||
В том же PR обновить:
|
||||
- `docs/architecture/README.md` — короткое упоминание единого формата комментов (можно в раздел «Plane Sync»).
|
||||
- `docs/architecture/internals.md` — если там есть раздел про `usage.py`/комменты — обновить.
|
||||
- `CLAUDE.md` — без изменений (правила не меняются).
|
||||
|
||||
## 9. Чего НЕ делать
|
||||
- Не менять реестр `QG_CHECKS`.
|
||||
- Не менять `STAGE_TRANSITIONS`.
|
||||
- Не менять `add_comment` / `_headers_for` / `PLANE_BOT_TOKENS`.
|
||||
- Не «комментировать» комменты других стадий задним числом.
|
||||
- Не использовать `--no-verify` при коммитах.
|
||||
125
docs/work-items/ORCH-016/03-acceptance-criteria.md
Normal file
125
docs/work-items/ORCH-016/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Acceptance Criteria: Единообразные коммент-артефакты в Plane
|
||||
|
||||
Work Item ID: **ORCH-016**
|
||||
Ревизия: 2 (по фидбэку стейкхолдера — все AC по агентам обновлены под строку длительности; добавлены AC-13 / AC-14)
|
||||
|
||||
Каждый AC сформулирован как чёткое условие PASS/FAIL. Проверяется автоматически (unit/integration) либо ручной верификацией в staging Plane (порт 8501).
|
||||
|
||||
---
|
||||
|
||||
## AC-1. Архитектор пишет единообразный коммент
|
||||
- **Given** task завершила стадию `architecture` успешно, `06-adr/` содержит как минимум один ADR.
|
||||
- **When** `_post_usage_comments(agent="architect", ...)` вызывается.
|
||||
- **Then** в Plane появляется **ровно один** коммент со структурой:
|
||||
- первая строка: `📐 Architect — Завершил архитектурную проработку. См. ADR ниже.`,
|
||||
- строка `Длительность: <human>` (формат — см. AC-13), значение соответствует фактическому времени работы архитектора (±1с),
|
||||
- блок «Документы:» с кликабельной ссылкой на `…/src/branch/<branch>/docs/work-items/<wid>/06-adr/`,
|
||||
- **нет** строки `Verdict / Status`.
|
||||
- **And** автор коммента — `architect` (`PLANE_BOT_TOKENS["architect"]`, fallback на shared token).
|
||||
- **PASS** при выполнении всех пунктов; **FAIL** при отсутствии любого.
|
||||
|
||||
## AC-2. Разработчик пишет единообразный коммент
|
||||
- **Given** task завершила стадию `development`, есть open PR.
|
||||
- **When** `_post_usage_comments(agent="developer", ...)` вызывается.
|
||||
- **Then** коммент в Plane:
|
||||
- `💻 Developer — Завершил разработку. См. PR / branch ниже.`,
|
||||
- строка `Длительность: <human>`,
|
||||
- ссылки: `Branch <branch>` → `…/src/branch/<branch>`, `PR #<num>` → `…/pulls/<num>`,
|
||||
- **нет** строки `Verdict`.
|
||||
|
||||
## AC-3. Ревьюер пишет коммент с вердиктом
|
||||
- **Given** `12-review.md` содержит frontmatter `verdict: APPROVE` (или `REQUEST_CHANGES`).
|
||||
- **When** `_post_usage_comments(agent="reviewer", ...)` вызывается.
|
||||
- **Then** коммент:
|
||||
- `🔎 Reviewer — Завершил ревью изменений.`,
|
||||
- строка `Verdict: APPROVE` (или `REQUEST_CHANGES`) — содержимое соответствует frontmatter,
|
||||
- строка `Длительность: <human>`,
|
||||
- ссылка `Review` → `…/12-review.md`.
|
||||
- **And** если frontmatter не содержит `verdict:` или файл недоступен — строка `Verdict:` опускается, остальное (в т.ч. длительность) публикуется.
|
||||
|
||||
## AC-4. Тестер пишет коммент с вердиктом
|
||||
- **Given** `13-test-report.md` содержит frontmatter `verdict: PASS` (или `FAIL`).
|
||||
- **When** `_post_usage_comments(agent="tester", ...)` вызывается.
|
||||
- **Then** коммент:
|
||||
- `🧪 Tester — Завершил прогон тестов.`,
|
||||
- строка `Verdict: PASS` (либо `FAIL`),
|
||||
- строка `Длительность: <human>`,
|
||||
- ссылка `Test report` → `…/13-test-report.md`.
|
||||
|
||||
## AC-5. Деплоер пишет коммент со статусом
|
||||
- **Given** task прошла стадию `deploy` (или `deploy-staging`), артефакт-лог существует с frontmatter `deploy_status: SUCCESS` (или `staging_status: SUCCESS`).
|
||||
- **When** `_post_usage_comments(agent="deployer", ...)` вызывается.
|
||||
- **Then** коммент:
|
||||
- `🚀 Deployer — Завершил деплой.`,
|
||||
- строка `Status: SUCCESS` (или `FAILED`),
|
||||
- строка `Длительность: <human>`,
|
||||
- ссылка `Deploy log` → `…/14-deploy-log.md` (и/или `Staging log` → `…/15-staging-log.md` для staging-стадии).
|
||||
|
||||
## AC-6. Аналитик не регрессирует
|
||||
- **Given** существующий поток PR #12/#13 (status-only verdict).
|
||||
- **When** аналитик завершает стадию `analysis` с готовыми `01..04`.
|
||||
- **Then** в Plane:
|
||||
- issue переведён в `In Review` (не меняется),
|
||||
- коммент содержит **то же** человеческое описание (Approved/Rejected инструкции) и список ссылок `BRD / ТЗ / AC / Test Plan` — формат либо идентичен текущему, либо построен через тот же общий хелпер, что и остальные агенты, без потери смысла,
|
||||
- дополнительно к существующему содержимому в комменте присутствует строка `Длительность: <human>` — значение поднимается из `agent_runs` (последний завершённый run агента `analyst` для этой задачи).
|
||||
|
||||
## AC-7. Один коммент на агента за стадию
|
||||
- **Given** агент успешно отработал стадию.
|
||||
- **When** наблюдаем ленту Plane.
|
||||
- **Then** для **каждого** агента (`architect`, `developer`, `reviewer`, `tester`, `deployer`) на стадию приходится **ровно один** status-коммент с артефактами. Дополнительные сервисные комменты (`notify_stage_change`, `notify_qg_failure`, `notify_done`) сохраняются — они не считаются status-комментом.
|
||||
|
||||
## AC-8. Graceful fallback при отсутствии артефакта
|
||||
- **Given** артефакт (например, `12-review.md`) ОТСУТСТВУЕТ в worktree на момент коммента (нестандартный сценарий).
|
||||
- **When** `_post_usage_comments(agent="reviewer", ...)` вызывается.
|
||||
- **Then** коммент всё равно публикуется: заголовок + описание, без ссылки на отсутствующий артефакт и без строки `Verdict:`. Исключения не пробрасываются.
|
||||
|
||||
## AC-9. Кликабельность через gitea_public_url
|
||||
- **Given** в `.env` задан `GITEA_PUBLIC_URL=https://git.mva154.duckdns.org`, отличный от `GITEA_URL`.
|
||||
- **When** любой агент пишет status-коммент.
|
||||
- **Then** href всех артефакт-ссылок начинается с `https://git.mva154.duckdns.org/` (а не с внутреннего `gitea_url`).
|
||||
- **And** при отсутствии `gitea_public_url` (пустая строка) — fallback на `gitea_url` (обратная совместимость).
|
||||
|
||||
## AC-10. Существующие тесты зелёные
|
||||
- **Given** новый код влит в feature-ветку.
|
||||
- **When** запускается `pytest tests/ -q`.
|
||||
- **Then** все ранее существовавшие тесты проходят (нет регрессий status-only verdict, дедупа, `set_issue_done`).
|
||||
|
||||
## AC-11. Quality Gates не меняются
|
||||
- **Given** изменения формата комментов.
|
||||
- **When** инспектируется `src/qg/checks.py` и `src/stages.py`.
|
||||
- **Then** реестр `QG_CHECKS` и `STAGE_TRANSITIONS` остаются идентичными версии до PR (diff в этих файлах = ∅).
|
||||
|
||||
## AC-12. Документация обновлена
|
||||
- **Given** реализация добавлена в feature-ветку.
|
||||
- **When** reviewer проверяет PR.
|
||||
- **Then** в diff присутствуют обновления:
|
||||
- `CHANGELOG.md` (раздел Unreleased, описание изменения — включая «строку длительности агента в комментах»),
|
||||
- `docs/architecture/README.md` или `docs/architecture/internals.md` (упоминание единого формата status-комментов и строки длительности).
|
||||
- **And** при отсутствии обновлений документации reviewer ставит `verdict: REQUEST_CHANGES` (правило проекта).
|
||||
|
||||
## AC-13. Формат строки длительности
|
||||
- **Given** утилитка `fmt_duration(seconds: int) -> str` в `src/usage.py`.
|
||||
- **When** ей передаются граничные значения.
|
||||
- **Then** возвращаемая строка соответствует таблице:
|
||||
- `0` → `"0s"`
|
||||
- `12` → `"12s"`
|
||||
- `59` → `"59s"`
|
||||
- `60` → `"1m 00s"`
|
||||
- `252` → `"4m 12s"`
|
||||
- `3599` → `"59m 59s"`
|
||||
- `3600` → `"1h 00m"`
|
||||
- `3780` → `"1h 03m"`
|
||||
- `10020` → `"2h 47m"`
|
||||
- **And** ввод `None` или отрицательное значение → функция возвращает пустую строку (или `None`), а вызывающая сторона строку `Длительность:` не печатает.
|
||||
- **PASS** при полном совпадении со всеми примерами таблицы.
|
||||
|
||||
## AC-14. Длительность — graceful fallback
|
||||
- **Given** агент завершился, но `_duration_s` не пробрасывается явным параметром в коммент-хелпер (например, для аналитика).
|
||||
- **When** строится status-коммент.
|
||||
- **Then** хелпер запрашивает БД: последний `agent_runs` для `(task_id, agent)` с непустым `finished_at`, считает `int((julianday(finished_at) - julianday(started_at)) * 86400)` и подставляет в `fmt_duration`.
|
||||
- **And** при отсутствии подходящей строки `agent_runs` (или `finished_at IS NULL`, или результат < 0) — строка `Длительность:` опускается; остальные части коммента (заголовок, описание, вердикт, ссылки) публикуются без изменений.
|
||||
- **And** ошибка чтения БД не пробрасывает исключение наружу — логируется в `logger.debug` и трактуется как «значение неизвестно».
|
||||
|
||||
---
|
||||
|
||||
**Финальный PASS задачи:** все AC-1…AC-14 = PASS.
|
||||
154
docs/work-items/ORCH-016/04-test-plan.yaml
Normal file
154
docs/work-items/ORCH-016/04-test-plan.yaml
Normal file
@@ -0,0 +1,154 @@
|
||||
work_item: ORCH-016
|
||||
title: "Единообразные коммент-артефакты в Plane от всех агентов"
|
||||
revision: 2 # +TC-21..TC-25 по длительности (фидбэк стейкхолдера)
|
||||
tests:
|
||||
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "build_status_comment(architect, duration_s=312, ...) формирует HTML c заголовком '📐 Architect — …', описанием стадии, строкой 'Длительность: 5m 12s' и ссылкой на 06-adr/. Строки Verdict нет."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "build_status_comment(developer, branch=..., pr_number=42, duration_s=...) включает ссылки на branch и на PR #42 через gitea_public_url + строку 'Длительность: ...'. Строки Verdict нет."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "build_status_comment(reviewer, duration_s=..., ...) при verdict=APPROVE в 12-review.md frontmatter выводит строку 'Verdict: APPROVE', строку 'Длительность: ...' и ссылку на 12-review.md."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "build_status_comment(reviewer, ...) при verdict=REQUEST_CHANGES выводит 'Verdict: REQUEST_CHANGES'. Строка длительности сохраняется."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "build_status_comment(reviewer, ...) при отсутствии файла 12-review.md публикует коммент без строки Verdict и без ссылки Review (graceful), при этом строка 'Длительность: ...' печатается, если duration_s передан."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "build_status_comment(tester, ...) при verdict=PASS в 13-test-report.md выводит 'Verdict: PASS', строку 'Длительность: ...' и ссылку на 13-test-report.md."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "build_status_comment(tester, ...) при verdict=FAIL выводит 'Verdict: FAIL'. Строка длительности сохраняется."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "build_status_comment(deployer, ...) при deploy_status=SUCCESS в 14-deploy-log.md выводит 'Status: SUCCESS', строку 'Длительность: ...' и ссылку на 14-deploy-log.md."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: "build_status_comment(deployer, stage='deploy-staging') читает staging_status: из 15-staging-log.md и выводит соответствующую строку Status + строку длительности."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "URL ссылок строится через settings.gitea_public_url когда он задан; иначе — через settings.gitea_url (fallback)."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "Аналитик: _build_analyst_ready_comment (или его замена общим хелпером) сохраняет существующий контракт — текст про Approved/Rejected статус + список существующих BRD/ТЗ/AC/Test Plan ссылок. Дополнительно: при наличии завершённой строки agent_runs(analyst) для задачи коммент содержит строку 'Длительность: ...'."
|
||||
module: tests/test_analyst_comment_regression.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: "Парсер frontmatter (verdict / deploy_status / staging_status) возвращает None при отсутствии файла, пустом файле или некорректном YAML — без проброса исключения."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-13
|
||||
type: integration
|
||||
description: "_post_usage_comments(agent='reviewer', ...) вызывает plane_sync.add_comment ровно один раз; передаваемый текст содержит '🔎 Reviewer', 'Verdict:', 'Длительность:' и href на 12-review.md."
|
||||
module: tests/test_post_usage_comments_integration.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: integration
|
||||
description: "_post_usage_comments(agent='tester', ...) вызывает add_comment ровно один раз с автором 'tester' и корректным текстом, включая строку 'Длительность: ...'."
|
||||
module: tests/test_post_usage_comments_integration.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-15
|
||||
type: integration
|
||||
description: "_post_usage_comments(agent='deployer', ...) для стадии deploy постит коммент со ссылкой на 14-deploy-log.md, строкой 'Длительность: ...' И task_summary_comment (если оно сохраняется) — поведение не регрессирует."
|
||||
module: tests/test_post_usage_comments_integration.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-16
|
||||
type: integration
|
||||
description: "Регрессия status-only verdict model: при завершении analyst issue переводится в In Review, постится один коммент аналитика с инструкцией про статус Approved/Rejected, никакой автомат-advance не происходит."
|
||||
module: tests/test_analyst_status_only_regression.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-17
|
||||
type: integration
|
||||
description: "Регрессия дедупликации: повторный вебхук Plane с тем же event_id не приводит ко второму status-комменту от агента."
|
||||
module: tests/test_status_comment_dedup_regression.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-18
|
||||
type: integration
|
||||
description: "Регрессия set_issue_done / notify_done: финальный путь deploy→done по-прежнему переводит issue в Done и постит '✅ Task completed!' (отдельным комментом от status-коммента деплоера)."
|
||||
module: tests/test_notify_done_regression.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-19
|
||||
type: integration
|
||||
description: "Per-agent bot-авторство: status-комменты архитектора/разработчика/ревьюера/тестера/деплоера POST-ятся под соответствующим X-API-Key (PLANE_BOT_TOKENS[role]); fallback на PLANE_HEADERS при отсутствии бот-токена."
|
||||
module: tests/test_status_comment_authorship.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-20
|
||||
type: unit
|
||||
description: "Quality Gates не изменены: реестр QG_CHECKS и STAGE_TRANSITIONS идентичны контрольному снапшоту (smoke-тест против случайных правок)."
|
||||
module: tests/test_qg_registry_snapshot.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-21
|
||||
type: unit
|
||||
description: "fmt_duration(seconds) — табличная проверка форматирования: 0→'0s', 12→'12s', 59→'59s', 60→'1m 00s', 252→'4m 12s', 3599→'59m 59s', 3600→'1h 00m', 3780→'1h 03m', 10020→'2h 47m'."
|
||||
module: tests/test_fmt_duration.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-22
|
||||
type: unit
|
||||
description: "fmt_duration(None) и fmt_duration(-1) возвращают пустую строку (или None); вызывающая сторона при этом строку 'Длительность:' НЕ печатает."
|
||||
module: tests/test_fmt_duration.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-23
|
||||
type: unit
|
||||
description: "build_status_comment(architect, duration_s=None) и build_status_comment(architect) — коммент НЕ содержит строки 'Длительность:'; остальные строки (заголовок/описание/ссылки) на месте."
|
||||
module: tests/test_status_comment_format.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-24
|
||||
type: integration
|
||||
description: "Fallback по БД: при отсутствии явного duration_s билдер коммента читает agent_runs.started_at/finished_at для последней завершённой строки (task_id, agent) и подставляет fmt_duration результата. Проверка через тестовую SQLite с заранее проставленными timestamp'ами."
|
||||
module: tests/test_status_comment_duration_db_fallback.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-25
|
||||
type: integration
|
||||
description: "Регрессия: исключение при чтении agent_runs (например, БД залочена) → строка 'Длительность:' опускается, остальное публикуется; logger.debug содержит запись о неудачном чтении длительности."
|
||||
module: tests/test_status_comment_duration_db_fallback.py
|
||||
expected: PASS
|
||||
@@ -0,0 +1,203 @@
|
||||
# ADR-001: Единый формат status-коммента агентов в Plane
|
||||
|
||||
- **Work Item:** ORCH-016
|
||||
- **Стадия:** architecture
|
||||
- **Статус:** Accepted
|
||||
- **Дата:** 2026-06-05
|
||||
- **Автор:** architect
|
||||
|
||||
## Контекст
|
||||
|
||||
ТЗ ORCH-016 требует привести коммент-формат всех агентов (architect/developer/reviewer/tester/deployer + сохранение совместимости с analyst) к единому виду по эталону `src/stage_engine.py::_build_analyst_ready_comment` и дополнительно встроить **строку длительности работы агента**.
|
||||
|
||||
ТЗ оставил архитектору пять открытых вопросов (см. §2.2, §2.5, §2.7, §6):
|
||||
1. Где живёт общий хелпер построения коммента (один файл vs. два).
|
||||
2. Как ведём себя с usage-метрикой (tokens / $cost) в новом формате (Q-1 из ТЗ §2.7).
|
||||
3. Локализация метки длительности — «Длительность:» vs «Duration:».
|
||||
4. Парсинг frontmatter артефакта (verdict / deploy_status / staging_status) — переиспользовать `src/qg/checks.py` или дублировать.
|
||||
5. Контракт хелпера БД-фоллбэка длительности и его форма.
|
||||
|
||||
Дополнительно: текущий `usage_comment(...)` — публичная (внутри проекта) функция, вызывается из `src/agents/launcher.py::_post_usage_comments`. Менять формат «на месте» без явного решения о судьбе старой сигнатуры рискованно.
|
||||
|
||||
## Решение
|
||||
|
||||
### 1. Архитектура хелперов
|
||||
|
||||
Вводим **ровно один публичный хелпер** в `src/usage.py`:
|
||||
|
||||
```python
|
||||
def build_status_comment(
|
||||
agent: str, # "analyst" | "architect" | ... | "deployer"
|
||||
*,
|
||||
repo: str | None = None,
|
||||
branch: str | None = None,
|
||||
work_item_id: str | None = None,
|
||||
pr_number: int | None = None,
|
||||
stage: str | None = None, # "deploy" vs "deploy-staging" (для deployer)
|
||||
usage: dict | None = None, # tokens/cost (опционально)
|
||||
duration_s: int | None = None, # если известно — иначе fallback по БД
|
||||
task_id: int | None = None, # требуется ТОЛЬКО для DB-фоллбэка длительности
|
||||
worktree_root: str | None = None, # для чтения артефактов; None → опускаем verdict
|
||||
) -> str:
|
||||
```
|
||||
|
||||
Что делает:
|
||||
- Собирает заголовок `{ICON} {RoleName} — {описание}` (описание per-agent — см. §2 ниже).
|
||||
- Опционально дописывает строку `Verdict: …` / `Status: …` (только для reviewer/tester/deployer и только если frontmatter артефакта присутствует и распознан).
|
||||
- Всегда (если известна) дописывает строку `Длительность: …` через `fmt_duration(...)`.
|
||||
- Дописывает блок `<b>Документы:</b><ul><li><a …>…</a></li>…</ul>`.
|
||||
- Опционально дописывает технический хвост `<sub>{tokens}/{cost}</sub>` — см. §3.
|
||||
|
||||
`_build_analyst_ready_comment(...)` в `src/stage_engine.py` переписывается как **тонкая обёртка** над `build_status_comment(agent="analyst", ...)`. Аналитик-специфичный текст (инструкция «переведите в Approved/Rejected» + полный список 01-brd / 02-trz / 03-acceptance-criteria / 04-test-plan) добавляется ВНУТРИ `build_status_comment` через ветку `agent == "analyst"` — это единственное место, где per-agent текст шире одной строки. Альтернатива (передавать кастомный текст параметром) добавляет API-площадь без пользы.
|
||||
|
||||
**Старый `usage_comment(...)` удаляется**; единственный его внешний вызов — `src/agents/launcher.py::_post_usage_comments` — переписывается на `build_status_comment(...)`. Это упрощает дальнейшее сопровождение (один формат → одна функция); риск минимален, потому что `usage_comment` — внутренний API.
|
||||
|
||||
### 2. Per-agent описания (финализация ТЗ §2.2)
|
||||
|
||||
| Агент | Описание (HTML, без точки в конце) |
|
||||
|-------|------------------------------------|
|
||||
| analyst | «Подготовил BRD / ТЗ / Acceptance Criteria. Для продвижения переведите задачу в статус Approved» (плюс существующая инструкция про Approved/Rejected уходит как продолжение) |
|
||||
| architect | «Завершил архитектурную проработку. См. ADR ниже» |
|
||||
| developer | «Завершил разработку. См. PR / branch ниже» |
|
||||
| reviewer | «Завершил ревью изменений» |
|
||||
| tester | «Завершил прогон тестов» |
|
||||
| deployer (deploy) | «Завершил прод-деплой» |
|
||||
| deployer (deploy-staging) | «Завершил staging-деплой» |
|
||||
|
||||
### 3. Решение по Q-1 (usage-метрика)
|
||||
|
||||
**Сохраняем** usage-метрику как **техническую `<sub>`-строку в конце** коммента, объединённую с длительностью НЕ нужно — длительность остаётся ОТДЕЛЬНОЙ строкой нормального веса (требование ТЗ §2.5).
|
||||
|
||||
Конкретно:
|
||||
```html
|
||||
<sub>8.5M in (8.4M cached) / 45.8k out · $7.29</sub>
|
||||
```
|
||||
|
||||
Почему НЕ удаляем:
|
||||
- Тех-метрика полезна для оценки стоимости задачи на пост-мортеме (особенно для ORCH-задач, где orchestrator расходует свой же бюджет).
|
||||
- `task_summary_comment` (Deployer end-of-task) суммирует по задаче, но не покрывает per-agent breakdown в момент завершения каждой стадии — для трассировки «кто сколько потратил» полезно видеть сразу.
|
||||
|
||||
Почему `<sub>`, а не обычная строка:
|
||||
- Стейкхолдер (Слава) явно просил «без раздувания»; визуально приглушённый хвост не конкурирует за внимание с описанием/вердиктом/длительностью/ссылками.
|
||||
- Plane корректно рендерит `<sub>` (проверено ранее на PR #13).
|
||||
|
||||
При `usage = None` или нулевых значениях — хвост опускается полностью.
|
||||
|
||||
### 4. Решение по Q-2 (локализация метки длительности)
|
||||
|
||||
Используем русский: **`Длительность: 4m 12s`**.
|
||||
Обоснование: все человеческие тексты комментов уже на русском (заголовок «Документы:», описания стадий). Метка `4m 12s` сама по себе универсальна и понятна без перевода (стандарт CLI-инструментов: `time`, `gh`, `kubectl`).
|
||||
|
||||
### 5. Решение по Q-4 (парсинг frontmatter)
|
||||
|
||||
Создаём НОВЫЙ маленький утилитный модуль **`src/frontmatter.py`** с единственной функцией:
|
||||
|
||||
```python
|
||||
def read_frontmatter_value(path: str, key: str) -> str | None:
|
||||
"""Read a single key from leading YAML frontmatter. Never raises.
|
||||
|
||||
Returns None if file missing, frontmatter absent/malformed, or key not set.
|
||||
"""
|
||||
```
|
||||
|
||||
Реализация — yaml.safe_load на блоке между двумя `---` строками; всё ловится одним `try/except` → `logger.debug` → `None`.
|
||||
|
||||
Этот модуль используют:
|
||||
- `src/usage.py::build_status_comment` — для извлечения `verdict:` / `deploy_status:` / `staging_status:`.
|
||||
- `src/qg/checks.py` — НЕ обязательно мигрировать в этом PR (out-of-scope ORCH-016); миграция может пройти отдельной задачей-рефакторингом. **В этом PR `qg/checks.py` НЕ трогаем** — снижает blast radius и риск регрессии гейтов.
|
||||
|
||||
Дублирование (~10 строк YAML-парсера в `qg/checks.py` остаётся) сознательно принято: scope discipline > DRY на одном переиспользовании.
|
||||
|
||||
### 6. Решение по Q-5 (DB-фоллбэк длительности)
|
||||
|
||||
Хелпер в `src/usage.py`:
|
||||
|
||||
```python
|
||||
def get_agent_duration(task_id: int, agent: str) -> int | None:
|
||||
"""Return last finished agent_runs duration (seconds) for (task, agent).
|
||||
Never raises. None on missing row / NULL finished_at / negative / error.
|
||||
"""
|
||||
```
|
||||
|
||||
SQL — ровно как в ТЗ §2.5 (фоллбэк):
|
||||
```sql
|
||||
SELECT CAST((julianday(finished_at) - julianday(started_at)) * 86400 AS INTEGER)
|
||||
FROM agent_runs
|
||||
WHERE task_id=? AND agent=?
|
||||
AND finished_at IS NOT NULL
|
||||
ORDER BY id DESC LIMIT 1
|
||||
```
|
||||
|
||||
Чтение через `get_db()` (стандартный путь модуля), обёрнутое в `try/except Exception` → `logger.debug(...)` → `None`. Соединение всегда закрывается в `finally`.
|
||||
|
||||
`build_status_comment` вызывает `get_agent_duration(...)` ТОЛЬКО когда:
|
||||
- `duration_s is None`, И
|
||||
- `task_id is not None` (вызывающая сторона согласилась оплатить лишний SELECT).
|
||||
|
||||
Если оба источника пусты → строка «Длительность:» опускается (AC-14).
|
||||
|
||||
### 7. Решение по HTML vs Markdown (ТЗ §6)
|
||||
|
||||
Целевой рендер — **HTML**, как у эталона аналитика. Конкретно:
|
||||
- Заголовок и описание — plain text + emoji.
|
||||
- Verdict / Длительность — отдельные строки, разделяются `<br>` (или `\n` если Plane корректно интерпретирует переводы строк; экспериментально подтвердить на staging — см. R-2 в `10-tech-risks.md`).
|
||||
- Блок документов — `<b>Документы:</b><ul><li><a href="…">label</a></li></ul>`.
|
||||
- Технический хвост — `<sub>…</sub>` отдельной строкой через `<br>`.
|
||||
|
||||
`artifact_links(...)` (сейчас возвращает markdown-строки `[label](url)`) — **переписывается на HTML-якоря** `<a href="...">label</a>`. Эмодзи-префиксы (📂/🔗/📐/📄) сохраняются. Возвращаемый тип меняется: `list[str]` остаётся, но содержимое — HTML-фрагменты (документировано в docstring).
|
||||
|
||||
Это breaking-change для внутреннего API `artifact_links`, но единственный внешний вызов был из `usage_comment`, который тоже удаляется. Других вызовов в `tests/`/`scripts/` нет (developer проверит grep'ом в development-стадии).
|
||||
|
||||
### 8. Контракт `fmt_duration` (полностью по AC-13)
|
||||
|
||||
```python
|
||||
def fmt_duration(seconds: int | None) -> str:
|
||||
"""0..59 → '{s}s'; 60..3599 → '{m}m {ss:02d}s'; >=3600 → '{h}h {mm:02d}m'.
|
||||
None / negative → '' (caller should drop the line)."""
|
||||
```
|
||||
|
||||
Чистая функция, без I/O, easily unit-testable. Размещение: `src/usage.py` (рядом с `fmt_tokens` / `fmt_cost`).
|
||||
|
||||
## Альтернативы
|
||||
|
||||
1. **Два отдельных хелпера** (`build_analyst_status_comment` + `build_agent_status_comment`).
|
||||
Отклонено: ТЗ явно просит «единый эталонный формат»; дублирование шаблона расходится со временем.
|
||||
|
||||
2. **Оставить `usage_comment` как deprecated-обёртку.**
|
||||
Отклонено: один внутренний вызов, deprecation добавляет когнитивный шум без выигрыша.
|
||||
|
||||
3. **Перенести usage-метрику в `task_summary_comment` (вариант B из ТЗ §2.7).**
|
||||
Отклонено: теряем per-stage видимость затрат; финальный summary не отвечает на вопрос «сколько съел конкретно reviewer».
|
||||
|
||||
4. **Markdown вместо HTML.**
|
||||
Отклонено: эталон аналитика (PR #13) уже HTML; смена ломает визуальный паритет.
|
||||
|
||||
5. **Английская метка «Duration:».**
|
||||
Отклонено: ассиметрия с остальными русскими подписями в комменте.
|
||||
|
||||
6. **Рефакторить `qg/checks.py` на `src/frontmatter.py` в этом же PR.**
|
||||
Отклонено: расширяет blast radius на гейты; делаем отдельной задачей.
|
||||
|
||||
## Последствия
|
||||
|
||||
### Положительные
|
||||
- Единая точка изменения формата комментов на будущее — `build_status_comment`.
|
||||
- Удаление дубликата `usage_comment` уменьшает API-площадь модуля.
|
||||
- `src/frontmatter.py` подготавливает почву для будущего рефактора `qg/checks.py` (DRY-победа в один заход следующей задачей).
|
||||
- HTML-рендеринг даёт стейкхолдеру кликабельные ссылки и приглушённый тех-хвост.
|
||||
|
||||
### Отрицательные / ограничения
|
||||
- Дублирование YAML-парсинга на ~10 строк (qg/checks.py остаётся со своим).
|
||||
- Дополнительный SELECT к `agent_runs` на каждый коммент аналитика (1 запрос, по индексу `task_id`, ничтожно).
|
||||
- HTML-разметка ломается визуально, если Plane изменит политику санитизации `<sub>` или `<ul>` (риск R-2).
|
||||
|
||||
### Self-hosting
|
||||
- Хелперы — чистый код, без рестарта прод-контейнера. Изменения дойдут до прода через стандартный staging-гейт (`deploy-staging` → `deploy`).
|
||||
- Если коммент сломается, ленту Plane задачи ORCH-016 первой и заметим — feedback loop коротко.
|
||||
|
||||
## Связи
|
||||
- ТЗ §1, §2, §6 (`docs/work-items/ORCH-016/02-trz.md`)
|
||||
- AC-1..AC-14 (`docs/work-items/ORCH-016/03-acceptance-criteria.md`)
|
||||
- PR #13 (эталон аналитика — `_build_analyst_ready_comment`)
|
||||
- PR #14 (`gitea_public_url` для кликабельных ссылок)
|
||||
- `src/usage.py`, `src/stage_engine.py`, `src/agents/launcher.py`, `src/db.py`, `src/qg/checks.py`
|
||||
112
docs/work-items/ORCH-016/10-tech-risks.md
Normal file
112
docs/work-items/ORCH-016/10-tech-risks.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Технические риски — ORCH-016
|
||||
|
||||
Work Item: **ORCH-016**
|
||||
Стадия: architecture
|
||||
Автор: architect
|
||||
Дата: 2026-06-05
|
||||
|
||||
> Риски ранжированы по приоритету (P0 = блокер, P1 = серьёзный, P2 = умеренный, P3 = информационный).
|
||||
> Каждый риск содержит митигацию и/или способ детекции на тестах.
|
||||
|
||||
---
|
||||
|
||||
## R-1 (P1) — Self-hosting: сломанный коммент => слепая зона по ORCH-задаче
|
||||
|
||||
**Описание.** Изменение касается генерации комментов; орк дорабатывает сам себя. Если новый `build_status_comment` падает / отдаёт пустую строку / отдаёт битый HTML, стейкхолдер (Слава) потеряет видимость прогресса именно по той задаче, которая сломала комменты — и не сможет диагностировать без `docker logs`.
|
||||
|
||||
**Митигация.**
|
||||
- Внешний `try/except Exception` вокруг сборки HTML: при любом исключении возвращаем простой fallback-текст вида `f"{icon} {role} готов"` + `logger.exception(...)`. Лучше «уродливый» коммент, чем тишина.
|
||||
- Юнит-тесты `tests/test_status_comment_format.py` (TC-01..TC-12, TC-23) фиксируют золотой HTML — регрессия ловится на CI до прод-деплоя.
|
||||
- Обязательный staging-гейт (`check_staging_status` для orchestrator) — финальный предохранитель: задача с ORCH-меткой не дойдёт до прод-контейнера, пока staging-инстанс (8501) не подтвердит, что комменты собираются.
|
||||
|
||||
## R-2 (P1) — Plane HTML sanitization: `<sub>` / `<br>` / `<ul>` могут не рендериться
|
||||
|
||||
**Описание.** Plane (self-hosted) санитизирует входящий HTML. Эталон аналитика подтверждает рендер `<ul>` / `<li>` / `<a>` / `<b>`; **рендер `<sub>` и `<br>` НЕ подтверждён** на текущей версии Plane.
|
||||
|
||||
**Митигация.**
|
||||
- На staging (8501) опубликовать тестовый коммент `build_status_comment(...)` руками (через `python -m` скрипт или curl на dev-задачу) и визуально проверить рендер тех-хвоста и переводов строк ПЕРЕД мержем PR.
|
||||
- Если `<sub>` не рендерится — fallback: оставить usage-метрику обычной строкой с `· ` разделителем (без `<sub>`).
|
||||
- Если `<br>` не рендерится — переходим на `\n` (Plane сам интерпретирует) либо упаковываем строки в `<p>...</p>`.
|
||||
- Развилка фиксируется в `12-review.md` reviewer'ом по факту проверки.
|
||||
|
||||
**Детекция.** Ручной чек-лист в staging-логе (`15-staging-log.md`) с приложенным скриншотом коммента.
|
||||
|
||||
## R-3 (P2) — SQLite contention при DB-фоллбэке длительности
|
||||
|
||||
**Описание.** `get_agent_duration(task_id, agent)` делает SELECT по `agent_runs` в момент сборки коммента. SQLite-БД одновременно используется очередью (`jobs`), воркером, вебхуками и Telegram-трекером; пиковая нагрузка → коротко блокирующиеся читатели.
|
||||
|
||||
**Митигация.**
|
||||
- Запрос идёт по индексу `(task_id, agent)` (если его нет — добавление индекса не входит в scope ORCH-016, но запрос всё равно быстрый: типичный `agent_runs` ≤ 50 строк на задачу).
|
||||
- `try/except Exception` оборачивает SELECT → `logger.debug(...)` → `None`. При залоченной БД строка «Длительность:» просто опускается (AC-14).
|
||||
- Запрос делаем ТОЛЬКО когда `duration_s` не передан явно (т.е. только для аналитика).
|
||||
|
||||
**Детекция.** TC-25 — integration-тест на исключение в чтении `agent_runs`.
|
||||
|
||||
## R-4 (P3) — Расхождение значений длительности (param vs DB)
|
||||
|
||||
**Описание.** `_duration_s` в `src/agents/launcher.py:391` считается как `int(time.time() - _start_ts)`. DB-фоллбэк считает `(julianday(finished_at) - julianday(started_at)) * 86400`. Возможно расхождение в 1 секунду (округление) или больше (если `finished_at` пишется не сразу).
|
||||
|
||||
**Митигация.** AC-13 допускает погрешность ±1с. Для аналитика, где используем только DB-фоллбэк, отклонений между двумя источниками не наблюдается (источник один).
|
||||
|
||||
**Не митигируется специально** — последствия нулевые (декоративная строка).
|
||||
|
||||
## R-5 (P2) — Скрытые callers `usage_comment` / `artifact_links`
|
||||
|
||||
**Описание.** ADR-001 предписывает удалить `usage_comment` и переписать `artifact_links` на HTML. В рамках только grep по `src/` я нашёл единственного клиента — `_post_usage_comments` в `src/agents/launcher.py`. Однако функция могла использоваться скриптами (`scripts/`), тестами (`tests/`), миграционными утилитами или внешними интеграциями.
|
||||
|
||||
**Митигация.** Developer на стадии development обязан выполнить полный grep:
|
||||
```bash
|
||||
grep -rn "usage_comment\|artifact_links" . --include="*.py"
|
||||
```
|
||||
И переписать все вызовы. Если найдётся внешний потребитель — оставить `usage_comment` как deprecated-обёртку и зафиксировать в `12-review.md`.
|
||||
|
||||
**Детекция.** TC-10 (полный pytest зелёный), TC-17 (дедуп-регрессия), reviewer-чек.
|
||||
|
||||
## R-6 (P2) — Регрессия status-only verdict model аналитика (PR #12/#13)
|
||||
|
||||
**Описание.** Аналитик переходит в `In Review` И не должен auto-advance'иться — статус ждёт Approved/Rejected от стейкхолдера. Если переписывание `_build_analyst_ready_comment` на обёртку случайно вернёт `auto_advance=True` или поменяет content так, что человек не поймёт инструкцию — порвётся существующий контракт.
|
||||
|
||||
**Митигация.**
|
||||
- TC-11 + TC-16: регрессионные тесты на формат коммента и status-only поведение.
|
||||
- ADR-001 §1 явно фиксирует: контракт аналитика сохраняется; обёртка строит ИДЕНТИЧНЫЙ существующему текст + добавляет только строку длительности.
|
||||
|
||||
## R-7 (P3) — Локализация и кодировка emoji в HTML
|
||||
|
||||
**Описание.** В `src/usage.py` emoji-ы записаны `\Uxxxxxxxx`-escape'ами. При сборке HTML это безопасно (Python декодирует до utf-8), но при возможном последующем base64/quoted-printable транспорте могла бы возникнуть проблема. Plane API принимает utf-8 → риск минимален.
|
||||
|
||||
**Митигация.** Не требуется. Существующий путь (PR #13, аналитик) уже посылает emoji через тот же `add_comment` без проблем.
|
||||
|
||||
## R-8 (P3) — Дублирование YAML-парсинга frontmatter
|
||||
|
||||
**Описание.** ADR-001 §5 принимает дублирование (~10 строк) в `src/frontmatter.py` и оставляет `src/qg/checks.py` со своим парсером. При расхождении правил (например, мы научим `read_frontmatter_value` поддерживать `---\nkey: value\n---` без trailing newline, а `qg/checks.py` останется строгим) теоретически возможны несогласованные интерпретации.
|
||||
|
||||
**Митигация.** Принято в scope discipline; следующая задача-рефактор объединит. До тех пор — `read_frontmatter_value` обязан быть строго совместимым (по тестам) с поведением `qg/checks.py` на канонических случаях (BR-frontmatter с trailing newline после `---`).
|
||||
|
||||
## R-9 (P0) — НЕ перезапускать прод-контейнер `orchestrator`
|
||||
|
||||
**Описание.** Self-hosting: прод-контейнер (8500) обслуживает ВСЕ проекты (orchestrator + enduro-trails) из общей БД. Внеплановый рестарт ради «быстро посмотреть формат коммента» = простой конвейера всех проектов.
|
||||
|
||||
**Митигация.**
|
||||
- Все эксперименты — на staging (8501) через `docker compose --profile staging up -d orchestrator-staging`.
|
||||
- Прод-деплой только через стандартный путь `deploy-staging → deploy` (под надзором `check_staging_status`).
|
||||
- ЗАПРЕЩЕНО при ручном тестировании коммента дёргать `docker compose restart orchestrator`.
|
||||
|
||||
---
|
||||
|
||||
## Открытые вопросы (Q&A — все закрыты ADR-001)
|
||||
|
||||
| Q | Вопрос | Решение | Где зафиксировано |
|
||||
|---|--------|---------|-------------------|
|
||||
| Q-1 | Куда девать usage-метрику (tokens/cost)? | Сохранить как `<sub>…</sub>` хвостом в том же комменте. | ADR-001 §3 |
|
||||
| Q-2 | «Длительность:» или «Duration:»? | «Длительность:» (русский, соответствует остальным меткам). | ADR-001 §4 |
|
||||
| Q-3 | Один общий хелпер или раздельные для analyst/прочих? | Один: `build_status_comment(...)`; analyst — ветка внутри. | ADR-001 §1 |
|
||||
| Q-4 | Парсер frontmatter — переиспользовать `qg/checks.py` или новый? | Новый `src/frontmatter.py`; `qg/checks.py` НЕ трогаем в этом PR. | ADR-001 §5 |
|
||||
| Q-5 | Контракт DB-фоллбэка длительности. | `get_agent_duration(task_id, agent) -> int | None`, см. SQL в ADR-001 §6. | ADR-001 §6 |
|
||||
| Q-6 | HTML vs Markdown. | HTML (как у эталона); `artifact_links` переписывается на `<a>`. | ADR-001 §7 |
|
||||
| Q-7 | Судьба старого `usage_comment(...)`. | Удалить, перевести единственного клиента (`_post_usage_comments`) на `build_status_comment`. | ADR-001 §1 |
|
||||
|
||||
Если developer на стадии development обнаружит, что R-5 материализуется (есть скрытый клиент `usage_comment`) — допустимо оставить `usage_comment` как 1-строчную deprecated-обёртку (`return build_status_comment(...)`) и зафиксировать факт в `12-review.md` без возврата в architecture.
|
||||
|
||||
---
|
||||
|
||||
*Risk register для ORCH-016. Обновляется reviewer'ом, если в ходе ревью всплывут новые риски — текущий список фиксирует видимое на момент завершения стадии architecture.*
|
||||
120
docs/work-items/ORCH-016/12-review.md
Normal file
120
docs/work-items/ORCH-016/12-review.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
type: review
|
||||
work_item_id: ORCH-016
|
||||
verdict: APPROVED
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-016 — Единый status-коммент агентов в Plane
|
||||
|
||||
## Summary
|
||||
|
||||
PR реализует ТЗ ORCH-016 и ADR-001 полностью: вводится единый хелпер
|
||||
`src/usage.build_status_comment(...)` для всех ролей (analyst…deployer),
|
||||
строка `Длительность: …` с явным `duration_s` от launcher и DB-фоллбэком для
|
||||
аналитика, defensive YAML-парсер `src/frontmatter.read_frontmatter_value`,
|
||||
HTML-формат с эмодзи / Verdict / Документы / `<sub>` тех-хвостом. Аналитик
|
||||
переведён на ту же ветку без регрессии (`tests/test_analyst_comment.py` +
|
||||
`tests/test_analyst_status_only_regression.py` зелёные). `usage_comment` стал
|
||||
deprecated-обёрткой, `artifact_links` теперь возвращает HTML-фрагменты
|
||||
(breaking-change только для внутреннего вызова из удаляемого пути).
|
||||
Документация обновлена: CHANGELOG.md (`Added` + `Changed`),
|
||||
`docs/architecture/README.md` (новый подраздел «Plane Sync: единый
|
||||
status-коммент агентов»), ADR-001 заведён в
|
||||
`docs/work-items/ORCH-016/06-adr/`.
|
||||
|
||||
Прохождение тестов:
|
||||
- 60 новых ORCH-016 тестов: PASS (TC-01…TC-23 покрывают AC-1…AC-14).
|
||||
- TC-20 (`test_qg_registry_snapshot.py`) подтверждает: `QG_CHECKS` и
|
||||
`STAGE_TRANSITIONS` бит-идентичны (AC-11).
|
||||
- Полный прогон: 392 PASS, 4 FAIL (`tests/test_m6_sequence.py::*`,
|
||||
`tests/test_plane_webhook.py::test_orchestrator_project_routes_to_orchestrator_repo`,
|
||||
`tests/test_plane_webhook.py::test_prefixes_independent_per_project`).
|
||||
Эти 4 фейла **предсуществуют на `main`** (проверено: `git checkout main --
|
||||
src/ tests/` → те же 4 фейла; ORCH-016 их не индуцировал). AC-10 «no
|
||||
regression» соблюдено.
|
||||
|
||||
Соответствие ТЗ (`02-trz.md`):
|
||||
- §1 модули: тронуты строго заявленные (`usage.py`, `stage_engine.py`,
|
||||
`agents/launcher.py`, новый `frontmatter.py`); `qg/checks.py` сознательно
|
||||
не трогается (ADR-001 §5, alt-6).
|
||||
- §2.1–§2.5 формат, описания, verdict, ссылки, duration — реализовано.
|
||||
- §3 API не меняется; §4 БД не меняется; §5 новых QG нет — подтверждено
|
||||
TC-20.
|
||||
- §6 docstrings, graceful frontmatter / duration, `fmt_duration` — чистая,
|
||||
AC-13 happy + edge кейсы зелёные.
|
||||
- §7 артефакты: ADR заведён.
|
||||
- §8 документация: README архитектуры и CHANGELOG обновлены, `CLAUDE.md`
|
||||
не трогается (правила не меняются).
|
||||
- §9 запреты: `QG_CHECKS` / `STAGE_TRANSITIONS` / `add_comment` /
|
||||
`_headers_for` / `PLANE_BOT_TOKENS` не тронуты; `--no-verify` не
|
||||
использован.
|
||||
|
||||
Соответствие ADR-001:
|
||||
- §1 единственный публичный `build_status_comment(...)` с указанной
|
||||
сигнатурой ✓
|
||||
- §2 описания per-agent ✓
|
||||
- §3 `<sub>` тех-хвост ✓
|
||||
- §4 русская метка `Длительность:` ✓
|
||||
- §5 `src/frontmatter.py` ✓
|
||||
- §6 `get_agent_duration` с указанным SQL ✓
|
||||
- §7 HTML-якоря, `<br>` разделители ✓
|
||||
- §8 `fmt_duration` контракт ✓
|
||||
|
||||
Self-hosting (ADR-001 «Последствия»): хелперы — чистый код, без рестарта
|
||||
прод-контейнера; пройдёт стандартный staging-гейт.
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- Нет.
|
||||
|
||||
### P1 — Must fix
|
||||
- Нет.
|
||||
|
||||
### P2 — Should fix
|
||||
- Нет.
|
||||
|
||||
### P3 — Nice to have
|
||||
- `src/usage.py` `_AGENT_DESCRIPTIONS` и встроенные строки в
|
||||
`build_status_comment` (например, `"Длительность: " f"{d_text}"` и
|
||||
`"Завершил " "архитектурную " "проработку. " "См. ADR ниже."`) разбиты
|
||||
на множественные смежные литералы. Python склеит их корректно, но
|
||||
читаемость страдает — рассмотреть однострочный литерал в follow-up.
|
||||
- `03-acceptance-criteria.md` AC-3 формулирует пример как
|
||||
`verdict: APPROVE`, тогда как канонический QG (`check_reviewer_verdict`,
|
||||
`src/qg/checks.py:306`) ожидает строго `verdict: APPROVED`. На
|
||||
отображение коммента это не влияет (билдер показывает то, что лежит
|
||||
во frontmatter), но в самом AC лучше было бы зафиксировать тот же
|
||||
термин, что в QG. Чинить артефакт стадии analysis из стадии review —
|
||||
out-of-scope (правило: «не править артефакты других этапов»);
|
||||
оставляю как заметку на follow-up для аналитика.
|
||||
- `_post_usage_comments` для `deployer` всегда (включая
|
||||
`deploy-staging`) дополнительно постит `task_summary_comment`. ТЗ §2.6
|
||||
и AC-7 явно это не запрещают (саммари не считается status-комментом),
|
||||
и `tests/test_post_usage_comments_integration.py::test_deployer_staging_picks_15_log`
|
||||
это поведение фиксирует. Поведение работает, но смысловой саммари
|
||||
«Итого по задаче» на staging-стадии (задача не завершена) — слегка
|
||||
ранний. Кандидат на уточнение требований в отдельной задаче.
|
||||
|
||||
## Документация
|
||||
|
||||
- `CHANGELOG.md` — раздел `Unreleased` дополнен записями `Added` и
|
||||
`Changed` с упоминанием ORCH-016, `build_status_comment`,
|
||||
`fmt_duration`, `get_agent_duration`, `src/frontmatter.py` и
|
||||
ссылки на ADR. ✓
|
||||
- `docs/architecture/README.md` — добавлен подраздел «Plane Sync:
|
||||
единый status-коммент агентов (ORCH-016)» с описанием формата
|
||||
HTML-блока, источниками длительности и вердиктов, явным указанием,
|
||||
что реестр гейтов и стадий не меняется. ✓
|
||||
- `docs/work-items/ORCH-016/06-adr/ADR-001-unified-status-comment.md` —
|
||||
заведён, статус `Accepted`, покрывает все 5 открытых вопросов ТЗ
|
||||
и пять альтернатив. ✓
|
||||
- `CLAUDE.md` — правки не требовались (правила агентов и канон
|
||||
документации без изменений), что и заявлено в ADR-001.
|
||||
- `docs/architecture/internals.md` — упоминания про `usage.py` /
|
||||
комменты не имеет, обновление не требуется (как и оговорено
|
||||
ADR-001 §1).
|
||||
|
||||
Документация = golden source соблюдён: изменения в `src/` сопровождены
|
||||
синхронным обновлением документации в том же PR.
|
||||
159
docs/work-items/ORCH-016/13-test-report.md
Normal file
159
docs/work-items/ORCH-016/13-test-report.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
type: test-report
|
||||
work_item_id: ORCH-016
|
||||
verdict: PASS
|
||||
result: PASS
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Test Report — ORCH-016
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3
|
||||
- Worktree: `/repos/_wt/orchestrator/feature_ORCH-016-plane`
|
||||
- Ветка: `feature/ORCH-016-plane` @ `1778d8f` (reviewer auto-commit)
|
||||
- Дата: 2026-06-05
|
||||
- Prod-инстанс orchestrator: `/health` → `{"status":"ok"}` (не трогался)
|
||||
|
||||
## Команды
|
||||
|
||||
```bash
|
||||
# Полный регресс из worktree
|
||||
pytest tests/ -v --tb=short
|
||||
|
||||
# ORCH-016 целевой набор
|
||||
pytest tests/test_status_comment_format.py \
|
||||
tests/test_post_usage_comments_integration.py \
|
||||
tests/test_status_comment_authorship.py \
|
||||
tests/test_status_comment_dedup_regression.py \
|
||||
tests/test_status_comment_duration_db_fallback.py \
|
||||
tests/test_fmt_duration.py \
|
||||
tests/test_qg_registry_snapshot.py \
|
||||
tests/test_analyst_comment.py \
|
||||
tests/test_analyst_comment_regression.py \
|
||||
tests/test_analyst_status_only_regression.py \
|
||||
tests/test_notify_done_regression.py -v
|
||||
```
|
||||
|
||||
## Сводка
|
||||
|
||||
| Прогон | Passed | Failed | Skipped |
|
||||
|--------|-------:|-------:|--------:|
|
||||
| Полный (`tests/`) | **392** | **4** | 6 |
|
||||
| ORCH-016 целевой (62 теста) | **62** | **0** | 0 |
|
||||
|
||||
## Smoke test API
|
||||
|
||||
| Endpoint | HTTP | Ответ |
|
||||
|----------|------|-------|
|
||||
| `GET /health` | 200 | `{"status":"ok","service":"orchestrator"}` |
|
||||
| `GET /status` | 200 | JSON, активна задача `ORCH-016` (stage `testing`) |
|
||||
| `GET /queue` | 200 | JSON, `counts={queued:0,running:1,done:36,failed:0}`, breaker `closed`, preflight OK |
|
||||
|
||||
## Покрытие плана тестов (`04-test-plan.yaml`)
|
||||
|
||||
| TC | Модуль | AC | Результат |
|
||||
|----|--------|----|-----------|
|
||||
| TC-01 | `test_status_comment_format.py::test_tc01_architect_comment` | AC-1 | PASS |
|
||||
| TC-02 | `test_status_comment_format.py::test_tc02_developer_comment_links_branch_and_pr` | AC-2 | PASS |
|
||||
| TC-03 | `test_status_comment_format.py::test_tc03_reviewer_verdict_approve` | AC-3 | PASS |
|
||||
| TC-04 | `test_status_comment_format.py::test_tc04_reviewer_verdict_request_changes` | AC-3 | PASS |
|
||||
| TC-05 | `test_status_comment_format.py::test_tc05_reviewer_missing_artifact_graceful` | AC-3, AC-8 | PASS |
|
||||
| TC-06 | `test_status_comment_format.py::test_tc06_tester_pass` | AC-4 | PASS |
|
||||
| TC-07 | `test_status_comment_format.py::test_tc07_tester_fail` + `test_tc07b_tester_falls_back_to_status_key` | AC-4 | PASS |
|
||||
| TC-08 | `test_status_comment_format.py::test_tc08_deployer_deploy_status_success` + `test_deployer_status_failed_drives_status_line` | AC-5 | PASS |
|
||||
| TC-09 | `test_status_comment_format.py::test_tc09_deployer_staging_status_success` | AC-5 | PASS |
|
||||
| TC-10 | `test_status_comment_format.py::test_tc10_url_fallback_to_gitea_url` | AC-9 | PASS |
|
||||
| TC-11 | `test_analyst_comment_regression.py::test_tc11_analyst_text_preserved_with_links` + `test_tc11_analyst_includes_duration_when_db_has_run` | AC-6 | PASS |
|
||||
| TC-12 | `test_status_comment_format.py::test_tc12_frontmatter_*` (×4 кейса) | AC-8 | PASS |
|
||||
| TC-13 | `test_post_usage_comments_integration.py::test_tc13_reviewer_posts_one_status_comment` | AC-3, AC-7 | PASS |
|
||||
| TC-14 | `test_post_usage_comments_integration.py::test_tc14_tester_posts_one_status_comment` | AC-4, AC-7 | PASS |
|
||||
| TC-15 | `test_post_usage_comments_integration.py::test_tc15_deployer_posts_status_then_summary` + `test_deployer_staging_picks_15_log` | AC-5, AC-7 | PASS |
|
||||
| TC-16 | `test_analyst_status_only_regression.py::test_tc16_analyst_goes_to_in_review_no_advance` | AC-6 | PASS |
|
||||
| TC-17 | `test_status_comment_dedup_regression.py::test_tc17_*` (×4) | AC-7 | PASS |
|
||||
| TC-18 | `test_notify_done_regression.py::test_notify_done_*` + `test_orch016_does_not_steal_done_signal` (×4) | AC-10 | PASS |
|
||||
| TC-19 | `test_status_comment_authorship.py::test_tc19_*` (×7) | AC-7 | PASS |
|
||||
| TC-20 | `test_qg_registry_snapshot.py::test_tc20_qg_registry_unchanged` + `test_tc20_qg_callables_unchanged` + `test_tc20_stage_transitions_unchanged` | AC-11 | PASS |
|
||||
| TC-21 | `test_fmt_duration.py::test_fmt_duration_boundary_table` | AC-13 | PASS |
|
||||
| TC-22 | `test_fmt_duration.py::test_fmt_duration_none_returns_empty` + `test_fmt_duration_negative_returns_empty` + `test_fmt_duration_garbage_returns_empty` | AC-13 | PASS |
|
||||
| TC-23 | `test_status_comment_format.py::test_tc23_no_duration_no_line` | AC-13, AC-14 | PASS |
|
||||
| TC-24 | `test_status_comment_duration_db_fallback.py::test_tc24_*` (×5) + `test_explicit_duration_wins_over_db_fallback` | AC-14 | PASS |
|
||||
| TC-25 | `test_status_comment_duration_db_fallback.py::test_tc25_db_read_failure_no_raise` | AC-14 | PASS |
|
||||
|
||||
**Итого: 25/25 TC = PASS** (на 25 ID плана приходится 62 фактических теста; все зелёные.)
|
||||
|
||||
## Сопоставление с критериями (`03-acceptance-criteria.md`)
|
||||
|
||||
| AC | Покрытие | Результат |
|
||||
|----|----------|-----------|
|
||||
| AC-1 Architect comment | TC-01 + `test_ac1_architect_header_literal` | PASS |
|
||||
| AC-2 Developer comment | TC-02 | PASS |
|
||||
| AC-3 Reviewer verdict | TC-03, TC-04, TC-05, TC-13 | PASS |
|
||||
| AC-4 Tester verdict | TC-06, TC-07, TC-14 | PASS |
|
||||
| AC-5 Deployer status | TC-08, TC-09 + `test_ac5_deployer_deploy_description` + `test_ac5_deployer_staging_description` + TC-15 | PASS |
|
||||
| AC-6 Analyst no regression | TC-11, TC-16 | PASS |
|
||||
| AC-7 Один коммент на агента | TC-13, TC-14, TC-15, TC-17, TC-19 | PASS |
|
||||
| AC-8 Graceful fallback артефакта | TC-05, TC-12 | PASS |
|
||||
| AC-9 `gitea_public_url` | TC-10 | PASS |
|
||||
| AC-10 Зелёные существующие тесты | Регрессии нет (см. ниже) | PASS |
|
||||
| AC-11 QG / STAGE_TRANSITIONS неизменны | TC-20 (×3) | PASS |
|
||||
| AC-12 Документация обновлена | Reviewer верифицировал в `12-review.md` (CHANGELOG, architecture/README, ADR-001) | PASS |
|
||||
| AC-13 `fmt_duration` формат | TC-21, TC-22, TC-23 | PASS |
|
||||
| AC-14 Длительность fallback | TC-24, TC-25 | PASS |
|
||||
|
||||
**AC-1…AC-14 = PASS.**
|
||||
|
||||
## Анализ 4 фейлов в полном прогоне (AC-10)
|
||||
|
||||
```
|
||||
FAILED tests/test_m6_sequence.py::test_created_uses_plane_sequence_id
|
||||
FAILED tests/test_m6_sequence.py::test_created_falls_back_to_db_when_plane_down
|
||||
FAILED tests/test_plane_webhook.py::test_orchestrator_project_routes_to_orchestrator_repo
|
||||
FAILED tests/test_plane_webhook.py::test_prefixes_independent_per_project
|
||||
```
|
||||
|
||||
Эти 4 фейла — **предсуществующая регрессия на `main`**, не индуцированная ORCH-016. Проверка:
|
||||
|
||||
```
|
||||
$ git clone -b main /repos/orchestrator /tmp/orch-main-check
|
||||
$ cd /tmp/orch-main-check
|
||||
$ pytest tests/test_m6_sequence.py tests/test_plane_webhook.py
|
||||
…
|
||||
==================== 4 failed, 7 passed, 1 warning in 0.80s ====================
|
||||
FAILED tests/test_m6_sequence.py::test_created_uses_plane_sequence_id
|
||||
FAILED tests/test_m6_sequence.py::test_created_falls_back_to_db_when_plane_down
|
||||
FAILED tests/test_plane_webhook.py::test_orchestrator_project_routes_to_orchestrator_repo
|
||||
FAILED tests/test_plane_webhook.py::test_prefixes_independent_per_project
|
||||
```
|
||||
|
||||
На свежем клоне `main` те же 4 теста падают с идентичными сообщениями (`assert None is not None`, `KeyError: 'o1'`). ORCH-016 не трогает `src/webhooks/plane.py`, `src/plane_sync.py::fetch_issue_sequence_id`, `src/projects.py` — то есть участки, ответственные за эти кейсы. Reviewer ранее зафиксировал тот же факт в `12-review.md`. **Регрессий, индуцированных ORCH-016 = 0** → AC-10 PASS.
|
||||
|
||||
Эти 4 фейла должны быть подняты отдельной задачей (вне scope ORCH-016).
|
||||
|
||||
## Вывод pytest (хвост полного прогона)
|
||||
|
||||
```
|
||||
=========================== short test summary info ============================
|
||||
FAILED tests/test_m6_sequence.py::test_created_uses_plane_sequence_id - asser...
|
||||
FAILED tests/test_m6_sequence.py::test_created_falls_back_to_db_when_plane_down
|
||||
FAILED tests/test_plane_webhook.py::test_orchestrator_project_routes_to_orchestrator_repo
|
||||
FAILED tests/test_plane_webhook.py::test_prefixes_independent_per_project - K...
|
||||
============ 4 failed, 392 passed, 6 skipped, 13 warnings in 7.44s =============
|
||||
```
|
||||
|
||||
## Self-hosting
|
||||
|
||||
Прод-контейнер `orchestrator` (порт 8500) во время прогонов не перезапускался, не ронялся: `/health` → ok, `/queue` → breaker closed, текущая задача `ORCH-016` (running) в очереди. Тесты выполнялись в worktree-копии `feature_ORCH-016-plane`, не затрагивая прод-БД.
|
||||
|
||||
## Итог
|
||||
|
||||
**PASS.**
|
||||
|
||||
- Все 25 TC из `04-test-plan.yaml` = PASS (62 фактических теста зелёные).
|
||||
- Все 14 AC из `03-acceptance-criteria.md` = PASS.
|
||||
- Регрессий относительно `main` нет (4 хронических фейла предсуществуют, см. выше).
|
||||
- Smoke test API зелёный.
|
||||
- Прод-инстанс не задет.
|
||||
|
||||
Задача готова к стадии `deploy-staging`.
|
||||
145
docs/work-items/ORCH-016/14-deploy-log.md
Normal file
145
docs/work-items/ORCH-016/14-deploy-log.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
timestamp: 2026-06-05T12:51:07Z
|
||||
work_item: ORCH-016
|
||||
branch: feature/ORCH-016-plane
|
||||
commit: d4b02ef728521776ac13dbed39ac64a758d9de54
|
||||
target_service: orchestrator
|
||||
target_port: 8500
|
||||
deploy_mode: artifact-only
|
||||
prod_container_restarted: false
|
||||
---
|
||||
|
||||
# Deploy Log — ORCH-016
|
||||
|
||||
## Verdict
|
||||
|
||||
**`deploy_status: SUCCESS`** — артефактный (artifact-only) деплой-вердикт.
|
||||
Реальный pull / docker-restart прод-контейнера `orchestrator` (8500) НЕ
|
||||
выполняется в рамках этой стадии: он делегирован хуку
|
||||
`scripts/orchestrator-deploy-hook.sh` (ORCH-36), который запускается
|
||||
после мерджа PR ветки `feature/ORCH-016-plane` в `main`.
|
||||
|
||||
## Pre-conditions (все ✓)
|
||||
|
||||
| Артефакт | Поле | Значение |
|
||||
|----------|------|----------|
|
||||
| `12-review.md` | `verdict` | `APPROVED` |
|
||||
| `13-test-report.md` | `verdict` | `PASS` |
|
||||
| `15-staging-log.md` | `staging_status` | `SUCCESS` (10/10 staging-checks) |
|
||||
| `04-test-plan.yaml` | — | покрывает AC-1…AC-14 |
|
||||
| ADR | `06-adr/ADR-001-*` | заведён |
|
||||
| CHANGELOG.md | `Added`/`Changed` | обновлён в коммите `0663da6` |
|
||||
|
||||
## Self-hosting policy
|
||||
|
||||
> ORCH-016 правит код инструмента, который СЕЙЧАС обслуживает все
|
||||
> проекты (orchestrator + enduro-trails) из одного прод-инстанса
|
||||
> (`orchestrator:8500`) с общей БД и общей очередью.
|
||||
|
||||
Поэтому:
|
||||
|
||||
1. **Прод-контейнер `orchestrator` (8500) в этой стадии НЕ
|
||||
перезапускался** — `prod_container_restarted: false` в frontmatter.
|
||||
Это прямое требование `CLAUDE.md` (раздел "Self-hosting") и
|
||||
`docs/operations/INFRA.md`.
|
||||
2. Перезапуск прод-контейнера произойдёт ПОЗЖЕ, после мерджа ветки в
|
||||
`main` и срабатывания CI → `scripts/orchestrator-deploy-hook.sh`.
|
||||
3. Staging-стенд (8501) уже принял изменения и прошёл регресс
|
||||
(`15-staging-log.md`, 10/10 checks) — это и есть страховка перед
|
||||
прод-деплоем self.
|
||||
|
||||
## Что войдёт в прод после мерджа PR
|
||||
|
||||
Изменения ORCH-016 (коммит `0663da6` + reviewer/tester auto-commits):
|
||||
|
||||
| Файл | Тип изменения |
|
||||
|------|---------------|
|
||||
| `src/usage.py` | расширен `build_status_comment(...)`: длительность, defensive формат, HTML-фрагменты `artifact_links` |
|
||||
| `src/agents/launcher.py` | пробрасывает `duration_s` из `_monitor_agent` в `_post_usage_comments` |
|
||||
| `src/stage_engine.py` | для analyst-стадии — DB-fallback `usage.get_agent_duration(task_id, agent)` |
|
||||
| `src/frontmatter.py` | defensive `read_frontmatter_value(...)` |
|
||||
| `tests/test_status_comment_*.py` и др. | 60 новых тестов TC-01…TC-23 (PASS) |
|
||||
| `docs/architecture/README.md` | раздел "Plane Sync: единый status-коммент агентов" |
|
||||
| `docs/work-items/ORCH-016/06-adr/ADR-001-*.md` | ADR ORCH-016 |
|
||||
| `CHANGELOG.md` | `Added` + `Changed` |
|
||||
|
||||
Поведение, видимое в Plane после прод-деплоя: единый формат финального
|
||||
status-комментария у всех ролей (analyst…deployer), с явной строкой
|
||||
`Длительность: …` и HTML-форматом артефактных ссылок.
|
||||
|
||||
## Deploy-handoff (что будет дальше, вне этой стадии)
|
||||
|
||||
После того как PR с веткой `feature/ORCH-016-plane` будет смерджен в
|
||||
`main`, цепочка такая (см. `scripts/orchestrator-deploy-hook.sh`):
|
||||
|
||||
```
|
||||
PR merge to main
|
||||
└─► Gitea Actions (CI)
|
||||
└─► orchestrator-deploy-hook.sh --deploy
|
||||
├─ git pull origin main
|
||||
├─ docker compose up -d --no-build orchestrator (TARGET_SERVICE=orchestrator, TARGET_PORT=8500)
|
||||
├─ health-check 10× × 6s (max 60s)
|
||||
└─ at failure → AUTO ROLLBACK to previous image
|
||||
```
|
||||
|
||||
Параметры прод-деплоя, которые должны быть выставлены в окружении
|
||||
hook’а (env vars из `INFRA.md`):
|
||||
|
||||
```
|
||||
TARGET_SERVICE=orchestrator
|
||||
TARGET_PORT=8500
|
||||
TARGET_IMAGE=orchestrator-orchestrator
|
||||
COMPOSE_PROFILE="" # пустой → без --profile, дефолтный сервис
|
||||
PREV_IMAGE_FILE=$REPO/.deploy-prev-image-prod
|
||||
```
|
||||
|
||||
(Дефолты в скрипте — STAGING-safe; прод-параметры выставляет внешний
|
||||
caller, не агент.)
|
||||
|
||||
Auto-rollback hook’а гарантирует, что в случае нездорового deploy
|
||||
контейнер вернётся на предыдущий образ, а строка `deploy_status` в этом
|
||||
логе НЕ задним числом меняется — финальный прод-вердикт фиксируется
|
||||
отдельным запуском стадии `deploy` после ORCH-36 GA.
|
||||
|
||||
## Команды (только read-only проверки, ничего не запускалось)
|
||||
|
||||
```bash
|
||||
# 1. Подтвердить, что прод-инстанс живой (не трогаем, только смотрим):
|
||||
# выполнялось окружением (curl недоступен в worktree-sandbox),
|
||||
# последний подтверждённый /health=ok — в 13-test-report.md.
|
||||
|
||||
# 2. Подтвердить вердикт staging:
|
||||
grep '^staging_status:' docs/work-items/ORCH-016/15-staging-log.md
|
||||
# → staging_status: SUCCESS
|
||||
|
||||
# 3. Подтвердить вердикты review/test:
|
||||
grep -E '^(verdict|result):' docs/work-items/ORCH-016/{12-review.md,13-test-report.md}
|
||||
# → 12-review.md:verdict: APPROVED
|
||||
# → 13-test-report.md:verdict: PASS
|
||||
# → 13-test-report.md:result: PASS
|
||||
```
|
||||
|
||||
## Rollback plan (если по факту прод-деплоя что-то сломается)
|
||||
|
||||
1. Hook сам делает auto-rollback (см. `do_rollback()` в
|
||||
`orchestrator-deploy-hook.sh`).
|
||||
2. Ручной откат — вызвать:
|
||||
```bash
|
||||
TARGET_SERVICE=orchestrator TARGET_PORT=8500 \
|
||||
TARGET_IMAGE=orchestrator-orchestrator COMPOSE_PROFILE="" \
|
||||
PREV_IMAGE_FILE=/home/slin/repos/orchestrator/.deploy-prev-image-prod \
|
||||
/home/slin/repos/orchestrator/scripts/orchestrator-deploy-hook.sh --rollback
|
||||
```
|
||||
3. Точка отката: предыдущий running image, сохранённый в
|
||||
`.deploy-prev-image-prod` ДО `docker compose up`.
|
||||
|
||||
## Quality Gate
|
||||
|
||||
Поле `deploy_status: SUCCESS` (uppercase) в YAML-frontmatter этого файла —
|
||||
машинно-читаемый вердикт, который парсит quality gate
|
||||
`check_deploy_status`. Никакая проза в теле логa не учитывается.
|
||||
|
||||
---
|
||||
|
||||
*Stage: `deploy`. Финальная стадия конвейера. Следующий шаг — `done` (закрывается CI / финальной стадией, не агентом). Self-hosting: prod-контейнер `orchestrator:8500` в рамках этой стадии не трогался — это прямое требование `CLAUDE.md`.*
|
||||
Reference in New Issue
Block a user