architect(ET): auto-commit from architect run_id=95
All checks were successful
CI / test (push) Successful in 12s

This commit is contained in:
2026-06-05 12:15:40 +00:00
parent 57a3f6c9f7
commit 1150cd9144
2 changed files with 315 additions and 0 deletions

View File

@@ -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`