Files
orchestrator/docs/work-items/ORCH-087/02-trz.md
claude-bot 61f23f8fed
All checks were successful
CI / test (push) Successful in 29s
analyst(ET): auto-commit from analyst run_id=428
2026-06-09 08:54:51 +03:00

118 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ТЗ — ORCH-087
Техническое задание для архитектора/разработчика. Конкретные изменения кода/БД с привязкой к BR (см. `01-brd.md`). Архитектурные РЕШЕНИЯ (выбор механизма зачистки сирот, выбор `bump`/`edit`, формула отсечки аномалий времени) принимает архитектор в ADR на основе G0 — здесь зафиксированы требования и точки врезки.
---
## 0. Задействованные модули `src/`
| Модуль | Роль в задаче |
|--------|---------------|
| `src/notifications.py` | `update_task_tracker` (bump/edit), `render_task_tracker`, `_stage_line`, итоговая строка времени, `plane_status_label`/`_card_status_label` (заголовок/deploy-цикл) |
| `src/db.py` | учёт `message_id` карточек задачи (BR-G1); колонка `agent_runs.effort` (BR-EFF); геттеры/сеттеры |
| `src/agents/launcher.py` | `_spawn`: стамп `resolve_agent_effort(agent)` в `agent_runs.effort` в момент запуска (BR-EFF) |
| `src/usage.py` | `short_model_name` (рядом — рендер эффорта); при необходимости — пробрасывать effort в строку стадии |
| `tests/test_notifications*.py`, `tests/test_*tracker*` | unit-покрытие |
**НЕ трогать** (BR-G6): `src/reconciler.py` / `tests/test_reconciler.py` — задача не требует их правок; пересечение с ORCH-86 неприемлемо. Если правка всё же понадобится — сохранить ORCH-086 (`skipped_terminal_total`, `state_uuid`-dedup, terminal-skip F-1) и явно проверить на merge-gate.
## 1. G0 — расследование (BR-G0) → ADR
- Исследование выполняется ДО кода: собрать факты по §4 BRD (логи орка `data/runs`, Telegram message_id, БД `tracker_message_id`/`stage` по ORCH-082), воспроизвести прогон на staging (8501), зафиксировать таблицу «стадия → (заголовок+тело в Telegram) vs (stage в БД)».
- Артефакт расследования и обоснованная рекомендация `bump` vs `edit``06-adr/ADR-NNN-tracker-orphan-cleanup.md`.
- Код фикса (G1G3) реализует выбранный в ADR механизм. ТЗ ниже задаёт ИНВАРИАНТЫ, которым любой выбранный механизм обязан удовлетворять.
## 2. G1 — гарантированная зачистка сирот (BR-G1)
**Требование-инвариант:** после любого `update_task_tracker` в чате не остаётся НИ ОДНОЙ ранее созданной карточки этой задачи, кроме текущей (в пределах 48ч-лимита Telegram).
Точка проблемы (текущий код, `update_task_tracker`, ветка `mode == "bump"`):
```python
if mid is not None:
delete_telegram(mid) # удаляется ТОЛЬКО последний mid
new_mid = send_telegram(text, disable_notification=True)
if new_mid is not None:
set_tracker_message_id(task_id, new_mid)
```
`tasks.tracker_message_id` — скаляр (последний `mid`). При рассинхроне (send→None / рестарт между delete и send / пересоздание / гонка / delete-fail+send-ok) прежние карточки теряют ссылку и осиротевают.
**Требования к решению (любой механизм из ADR):**
- R-1. Система должна знать обо ВСЕХ незакрытых `message_id` карточек задачи (а не только о последнем), чтобы подчищать их при следующем bump / на рассинхроне / при старте.
- R-2. Перед/в момент создания новой карточки удаляются ВСЕ известные незакрытые `message_id`; успешно удалённые (включая «already gone» по `_DELETE_GONE_MARKERS`) исключаются из учёта; не удалённые transient — остаются в учёте для повторной попытки.
- R-3. Новый `message_id` записывается в учёт ТОЛЬКО при успешном `send` (`new_mid is not None`) — transient send не должен обнулять/терять учёт (сохранить текущую защиту BR-6).
- R-4. Инвариант «одна карточка на задачу» и «не более одного `send` за вызов» сохраняются → дубликатов внутри вызова нет.
- R-5. **Кандидатные механизмы для ADR** (выбор за архитектором, не предрешать в коде до ADR):
- (A) bump + полный учёт `message_id` (новая таблица `tracker_messages(task_id, message_id, created_at, deleted_at)` ИЛИ JSON-массив в колонке `tasks.tracker_message_ids`), зачистка всех незакрытых;
- (B) переход дефолта на `edit` (нет сирот by design; теряется «карточка внизу» ORCH-042) — взвесить против фича-просьбы.
- R-6. Изменение схемы БД (если выбран вариант A) — строго аддитивное (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`), идемпотентное, restart-safe на живой общей прод-БД (данные enduro не трогаются). Детали данных — `08-data-requirements.md`.
## 3. G2 — заголовок отражает текущую стадию (BR-G2)
- Рендер `render_task_tracker` уже строит заголовок/статус-строку из `tasks.stage` (`plane_status_label``_card_status_label`). Замёрзший `To Analyse` — следствие осиротевшей карточки (G1), а не бага рендера.
- Требование: после фикса G1 единственная живая карточка всегда несёт заголовок текущей стадии. Регресс-тест: на каждой стадии заголовок/статус-строка соответствуют `stage` в БД (часть staging-воспроизведения G0 + unit на `plane_status_label`).
## 4. G3 — deploy-цикл на карточке (BR-G3)
- Проверить, что `_STAGE_STATUS_LABEL["deploy"]` (`⏸️ Awaiting Deploy — ожидание Confirm Deploy`) + live-overlay `_live_plane_branch_override` (`deploying`, `monitoring`) покрывают весь цикл `Awaiting Deploy → Deploying → Confirm Deploy → Monitoring → Done`.
- Если какой-то под-статус не отображается на соответствующей стадии — добить offline-label/overlay. `Done` рендерится из `stage == "done"`. Контракт never-raise и kill-switch `tracker_live_status` сохраняются.
## 5. BR-EFF — эффорт в строке стадии
**API/данные:**
- Новая колонка `agent_runs.effort TEXT` (миграция `_ensure_column(conn, "agent_runs", "effort", "TEXT")` в `src/db.py`, рядом с `model`).
- **Стамп в момент запуска** (`launcher._spawn`): сразу после резолва `effort = resolve_agent_effort(agent, project_id)` записать его в строку `agent_runs` (тот же `run_id`). Источник — РЕАЛЬНО ушедшее в `--effort` значение (`""`/без флага → сохранить пусто/`NULL`). Это надёжнее пересчёта (CLI не возвращает эффорт в result-JSON).
- Допустимо: расширить `INSERT INTO agent_runs (task_id, agent, effort) VALUES (?,?,?)` или отдельным `UPDATE agent_runs SET effort=? WHERE id=?` после резолва. Выбор — архитектор; значение должно соответствовать фактическому флагу запуска.
**Рендер** (`render_task_tracker._stage_line`):
- Текущий суффикс: `f" · {model}"` при наличии модели.
- Добавить эффорт рядом: формат `· opus-4-8 · xhigh` ИЛИ компактно `· opus-4-8/xhigh` (на усмотрение, выбрать единый). При пустом эффорте — суффикс эффорта опускается (как опускается модель при пустой `short_model_name`).
- Брать `effort` из строки `agent_runs` соответствующей стадии (последний завершённый run, как `model`). Допустим fallback на `resolve_agent_effort(agent)` для исторических строк без колонки.
**Ожидаемо:** developer-строка → `xhigh`; tester/deployer → `medium`; analyst/architect/reviewer → `high` (по таблице ORCH-41/081).
## 6. BR-G5 — честное и сходимое итоговое время
Текущая итоговая строка (`done`):
```python
wall = _duration_seconds(created_at, updated_at) # раздут: вся очередь+ожидание+застой
review_seconds = _duration_seconds(brd_review_started, brd_review_ended) # раздут при застое
"⏱️ Всего {wall} · агенты {agent_seconds} · твоё {review}"
```
Проблема: `wall ≠ agent_seconds + review_seconds` (незалогированные queue-паузы) → итог визуально «врёт»; `review_seconds` засчитывает застой/рассинхрон (ORCH-087: 392м).
**Требования (формула — за архитектором, G5 «КАК — архитектору»):**
- T-1. Чистое рабочее время агентов = `Σ _duration_seconds(started, finished)` по `agent_runs` (текущий `agent_seconds`) — **главная метрика**, оставить точной.
- T-2. Человеческое BRD-время — ТОЛЬКО фактическое: НЕ включать аномальный застой/рассинхрон (`brd_review` болтался открытым из-за рассинхрона In Review→Backlog). Ограничить разумным порогом ИЛИ считать только активные окна. Аномалия не должна показываться как «твоё время».
- T-3. Wall-clock — если показываем, помечать как «общее (с ожиданием)», НЕ выдавать за рабочее время.
- T-4. Итог должен СХОДИТЬСЯ: либо `wall = Σ(стадии) + Σ(паузы с подписью)`, либо не показывать wall как сумму. Прозрачность вместо «магического» числа.
- T-5. `agent_runs`-агрегация (`total_in/total_out/total_cost/agent_seconds`) и `💰`-строка — без регресса.
## 7. Изменения API (endpoints)
Нет новых/изменённых HTTP-endpoint. (Опционально — отразить учёт карточек/effort в read-only снимке `GET /queue`, если архитектор сочтёт нужным; не обязательно.)
## 8. Изменения схемы БД
- `agent_runs.effort TEXT` — аддитивно, идемпотентно (`_ensure_column`). **Обязательно.**
- Учёт `message_id` (BR-G1, если выбран вариант A) — аддитивная таблица `tracker_messages` ИЛИ колонка-массив `tasks.tracker_message_ids`. **Зависит от ADR.** Подробности — `08-data-requirements.md`.
- Существующие колонки/таблицы (`tasks.tracker_message_id`, `brd_review_*`, `agent_runs.model`) — не ломать; при варианте A сохранить обратную совместимость со скалярным `tracker_message_id` (миграция/со-существование).
## 9. Требования к новым QG-проверкам
Нет. `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, машинные вердикты гейтов — без изменений.
## 10. Артефакты pipeline, создаваемые/обновляемые
- `06-adr/ADR-NNN-tracker-orphan-cleanup.md` (G0 вывод + рекомендация bump/edit + механизм G1 + формула G5) — архитектор.
- Обновить `CLAUDE.md` (§ Нотификации) и `docs/architecture/README.md` (компонент Notifications) — отразить учёт карточек, эффорт-в-строке, честное время. **Golden source наравне с кодом.**
- `CHANGELOG.md``## [Unreleased]` запись (под `.gitattributes merge=union`).
## 11. Инварианты (не нарушать)
- never-raise во всём пути нотификаций; карточка всегда silent (`disable_notification`).
- «одна карточка на задачу»; ≤1 `send` за вызов `update_task_tracker`.
- Ссылки ORCH-067 (`plane_issue_link`), `disable_web_page_preview` ORCH-080 — сохранены.
- `STAGE_TRANSITIONS` / `QG_CHECKS` / стадии конвейера — без изменений.
- БР-G6: разработка/merge поверх свежего `origin/main` (ORCH-86); `reconciler.py` не эродировать.
- Миграции БД аддитивны и идемпотентны (общая прод-БД, enduro не трогать).