Files
orchestrator/docs/work-items/ORCH-087/02-trz.md

14 KiB
Raw Blame History

ТЗ — 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 edit06-adr/ADR-NNN-tracker-orphan-cleanup.md.
  • Код фикса (G1G3) реализует выбранный в ADR механизм. ТЗ ниже задаёт ИНВАРИАНТЫ, которым любой выбранный механизм обязан удовлетворять.

2. G1 — гарантированная зачистка сирот (BR-G1)

Требование-инвариант: после любого update_task_tracker в чате не остаётся НИ ОДНОЙ ранее созданной карточки этой задачи, кроме текущей (в пределах 48ч-лимита Telegram).

Точка проблемы (текущий код, update_task_tracker, ветка mode == "bump"):

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

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 не трогать).