# ТЗ — 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`. - Код фикса (G1–G3) реализует выбранный в 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 не трогать).