239 lines
27 KiB
Markdown
239 lines
27 KiB
Markdown
---
|
||
work_item: ORCH-020
|
||
stage: analysis
|
||
author_agent: analyst
|
||
status: ready-for-review
|
||
created_at: 2026-06-17
|
||
model_used: claude-opus-4-8
|
||
---
|
||
|
||
# 01 — BRD (бизнес-требования): ORCH-020 — Оценка задачи (прогноз стоимости/времени/story points), запускаемая статусом «Оценка»
|
||
|
||
Work Item: **ORCH-020** · Repo: **orchestrator** · Стадия: analysis
|
||
|
||
> **Revision после REJECT (Plane, 2026-06-17).** Заказчик отклонил предыдущий пакет: «**что я не
|
||
> увидел в БРД — как запускать оценку?** Я хотел бы переводить задачу в статус "Оценка", после чего
|
||
> запускался бы механизм оценки, и после завершения оценки задача бы меняла статус на backlog. На
|
||
> оценку я буду отправлять задачи **массово через Plane**. Также я могу **переоценивать задачи много
|
||
> раз**.» Этот раунд **переписывает модель триггера**: оценка теперь — **операторское действие,
|
||
> запускаемое выделенным Plane-статусом «Оценка»** (а не «автоматически для каждой задачи на
|
||
> `start_pipeline`», как в отклонённой версии). Прочие требования (что прогнозируем, куда пишем,
|
||
> леджер прогноз↔факт, leaf-инварианты) сохранены и согласованы с новым триггером. Полный пакет
|
||
> `01`–`04` supersede’ит прежний по mtime.
|
||
|
||
## 1. Бизнес-контекст и проблема
|
||
|
||
Заказчик планирует работу по бэклогу вручную и хочет **до отправки задачи в работу** видеть прогноз:
|
||
сколько задача будет стоить (токены × тариф = $), сколько займёт времени и насколько она сложна
|
||
(размер в story points). Сейчас этих данных до старта нет: оркестратор собирает фактуру
|
||
(`input_tokens`/`output_tokens`/`cache_*`/`cost_usd`/`model`/`effort`, тайминги
|
||
`agent_runs.started_at/finished_at`, `tasks.created_at/updated_at`) **только постфактум** через
|
||
`src/usage.py` (`task_usage_summary`, `agent_cost_totals`, `record_usage`). Контур **прогноза до
|
||
старта** отсутствует.
|
||
|
||
**Корень REJECT — отсутствовал способ ЗАПУСКА оценки.** Заказчик мыслит оценку как **операторский
|
||
жест в Plane**, а не как невидимый авто-шаг: он сам решает, какие задачи бэклога оценить, **массово**
|
||
переводит их в выделенный статус, получает прогнозы и продолжает планирование. Отклонённая версия
|
||
прятала триггер в `start_pipeline` («оценка обязательна для каждой задачи автоматически») и явно
|
||
называла точку триггера «реализационной деталью» — это и есть то, что заказчик «не увидел» и
|
||
отверг.
|
||
|
||
Цитаты заказчика (Plane, 2026-06-17):
|
||
- REJECT: «как запускать оценку? Я хотел бы **переводить задачу в статус "Оценка"**, после чего
|
||
запускался бы механизм оценки, и после завершения оценки задача бы **меняла статус на backlog**. На
|
||
оценку я буду отправлять задачи **массово через Plane**. Также я могу **переоценивать задачи много
|
||
раз**.»
|
||
- Раунд Needs Input: «В Plane есть поле оценка, туда и нужно записывать оценку. По факту завершения
|
||
задачи вписать в смежное поле… для оценки есть два поля.»; «Только Шаг 1, без выбора модели»;
|
||
«Модели не выбираем и не меняем. Это вне скоупа».
|
||
|
||
Установленные факты по коду (на которые опирается решение, не изобретать):
|
||
- **Прецедент «статус-триггер уже есть в платформе.** Plane-статусы — слой B (индикация, ORCH-066) и
|
||
НЕ управляют машиной стадий; но платформа уже имеет **операторские action-статусы**, запускающие
|
||
side-механизмы: **STOP** (ORCH-090, отмена задачи) и **Confirm Deploy** (ORCH-059, прод-деплой).
|
||
Оба разбираются в `webhooks/plane.py::handle_issue_updated` через
|
||
`proj_states.get("<key>")` и оба **намеренно отсутствуют** в `plane_sync._DEFAULT_STATES`
|
||
(fail-closed: доска без статуса → `None` → ветка не активируется). Статус «Оценка» — **третий
|
||
представитель этого же семейства**.
|
||
- **Маппинг имени статуса → логический ключ** — `plane_sync._PLANE_NAME_TO_KEY` (`"STOP"→"stop"`,
|
||
`"Confirm Deploy"→"confirm_deploy"`); `get_project_states` резолвит UUID статуса per-project из
|
||
Plane API.
|
||
- **Массовость — «бесплатно».** Plane multi-select по N задачам в статус «Оценка» порождает N
|
||
отдельных `issue.updated`-вебхуков (по одному на issue); каждый обрабатывается независимо. Отдельный
|
||
«batch-UX» в оркестраторе не требуется — массовость обеспечивает сам Plane.
|
||
- **Фактура для калибровки уже накоплена** (`agent_runs`, агрегаты `task_usage_summary` /
|
||
`agent_cost_totals`, тайминги). Это сырьё для «истории похожих задач».
|
||
- **Plane-поля существуют.** На issue присутствуют поля `estimate_point` (ОЦЕНКА) и `point` (ФАКТ);
|
||
estimate-система на проекте (`project.estimate`) на момент анализа **не настроена** — инфра-
|
||
предусловие (NFR-7).
|
||
- **Выбор модели/эффорта статичен по роли** (`resolve_agent_model`/`resolve_agent_effort`,
|
||
ORCH-41/74; дефолт `claude-opus-4-8`) и в этой задаче **не трогается** (Шаг 2 вне объёма).
|
||
- **leaf-паттерн платформы** (`serial_gate`/`coverage_gate`/`labels`/`lessons`/`cancel`): never-raise,
|
||
kill-switch `*_enabled`, `*_repos` CSV (пусто → self-hosting only), read-only блок в `GET /queue`.
|
||
|
||
## 2. Объём (scope)
|
||
|
||
### В объёме (Шаг 1 — Оценка, запускаемая статусом)
|
||
- **Триггер «Оценка» (ядро правки).** Перевод issue в выделенный Plane-статус **«Оценка»** запускает
|
||
механизм оценки этой задачи. Оператор делает это **вручную и массово** (multi-select в Plane).
|
||
- **Жизненный цикл статуса:** `Backlog → (оператор) «Оценка» → [оркестратор: оценка] → (оркестратор)
|
||
Backlog`. По завершении оценки оркестратор **сам возвращает** issue в статус **`Backlog`**.
|
||
- **Пере-оценка много раз.** Повторный перевод в «Оценка» переоценивает задачу заново (идемпотентно:
|
||
перезапись `estimate_point` и строки леджера). Применимо при изменении скоупа.
|
||
- **Прогноз четырёх величин:** стоимость ($), время, токены и **сложность в story points** из
|
||
фиксированной шкалы `{1, 2, 3, 5, 8}`.
|
||
- **Шкала story points (фиксированная, ответ Q-3 = вариант A):** `1` — мелкая docs/label/config;
|
||
`2` — небольшой фикс; `3` — средняя; `5` — сложная (код + тесты); `8` — эпик / разбивать.
|
||
- **Запись прогноза в Plane-поле `estimate_point`** (это ОЦЕНКА).
|
||
- **Запись факта в Plane-поле `point`** по завершении задачи (фактическая реализованная сложность в
|
||
story points из фактических токенов/времени/стоимости по той же шкале) — для калибровки.
|
||
- **Отображение прогноза на двух поверхностях** (ответ Q-5 = оба): Plane-коммент + пункт **«Оценка»**
|
||
в общей Telegram-карточке задачи (`src/notifications.py`) — **время, токены, стоимость**.
|
||
- **Локальный леджер прогноз↔факт** (фундамент петли калибровки, связь с ORCH-8): хранение прогноза,
|
||
факта и дельты, **ключ — `work_item_id`** (issue может ещё не иметь pipeline-задачи на момент
|
||
оценки — она на бэклоге).
|
||
|
||
### Вне объёма
|
||
- **Шаг 2 — адаптивный выбор моделей агентов** (ответы Q-1/Q-2: «Только Шаг 1, без выбора модели»;
|
||
«Модели не выбираем и не меняем. Это вне скоупа»). Горячий путь `resolve_agent_model`/
|
||
`resolve_agent_effort`/`_spawn` **не модифицируется**.
|
||
> **ACTION (поручение заказчика, Plane 16:34):** «заведи отдельную задачу в Plane для адаптивного
|
||
> выбора модели и укажи зависимость на мультипровайдеров (ORCH-13)». Создание Plane-issue — действие
|
||
> уровня заказчика/PM и **вне write-объёма аналитика** (Write ограничен `docs/work-items/<id>/*`).
|
||
> Фиксирую как обязательный follow-up: новый work item «Адаптивный выбор модели агента по сложности»
|
||
> с зависимостью на **ORCH-13**; оценщик сложности из ORCH-020 — его вход. Оператору: подтвердить
|
||
> создание или создать вручную.
|
||
- **Автопереключение трека по сложности** (связка с ORCH-19) — позже; здесь сложность лишь
|
||
вычисляется и публикуется как сигнал.
|
||
- **Авто-ретроспективщик / RICE-приоритизатор** (E2/E3 ORCH-8) — вне объёма; леджер — фундамент.
|
||
- **Автоматическая оценка КАЖДОЙ задачи на `start_pipeline`** — **исключена явно** (модель
|
||
отклонённой версии). Оценка — операторский on-demand жест через статус «Оценка».
|
||
- **Изменение тарифной/биллинговой модели** — используется существующий `cost_usd` из `usage.py`.
|
||
- **Новый «batch-UX»/массовый эндпоинт как ОСНОВНОЙ путь** — не нужен (массовость даёт Plane
|
||
multi-select → N вебхуков). Программный `POST /estimate*` допустим лишь как **опциональное**
|
||
удобство/диагностика, не как основной триггер (см. TRZ §4).
|
||
|
||
## 3. Заинтересованные стороны
|
||
- **Заказчик / владелец продукта (Слава)** — инициатор оценки (переводит задачи в «Оценка»),
|
||
потребитель прогноза для планирования бэклога; принимает результат.
|
||
- **Оркестратор (self-hosting)** — носитель функции; общий прод обслуживает и `enduro-trails`.
|
||
- **Будущая петля саморазвития (ORCH-8)** — потребитель леджера прогноз↔факт для калибровки.
|
||
- **ORCH-13 (мультипровайдерность)** — будущий потребитель сигнала сложности (через follow-up Шаг 2).
|
||
|
||
## 4. Бизнес-требования (BR)
|
||
|
||
### Триггер и жизненный цикл (ядро ревизии)
|
||
- **BR-T1 — Запуск оценки статусом «Оценка».** Перевод issue в выделенный Plane-статус **«Оценка»**
|
||
запускает оценку именно этой задачи. Это **единственный обязательный** способ запуска (массовый и
|
||
ручной), реализуемый по образцу операторских action-статусов STOP (ORCH-090) / Confirm Deploy
|
||
(ORCH-059).
|
||
- **BR-T2 — Авто-возврат в Backlog.** По завершении оценки (успех или best-effort-пропуск)
|
||
оркестратор **сам** переводит issue обратно в статус **`Backlog`**. Заказчик видит задачу
|
||
вернувшейся в бэклог с заполненным `estimate_point`.
|
||
- **BR-T3 — Массовость через Plane.** Массовый перевод N задач в «Оценка» (multi-select Plane)
|
||
оценивает все N; каждый issue обрабатывается независимо (N вебхуков). Отдельный массовый UX в
|
||
оркестраторе не требуется.
|
||
- **BR-T4 — Пере-оценка много раз (идемпотентно).** Повторный перевод задачи в «Оценка»
|
||
переоценивает её заново; прогноз и строка леджера **перезаписываются** (не дублируются). Число
|
||
пере-оценок не ограничено.
|
||
- **BR-T5 — Fail-closed статус.** На доске без статуса «Оценка» (enduro / частичная конфигурация /
|
||
Plane недоступен) триггер **не активируется** (ключ резолвится в `None`) — нулевая регрессия;
|
||
это инфра-предусловие (NFR-7), а не ошибка.
|
||
- **BR-T6 — Не нарушать машину стадий и in-flight работу.** Статус «Оценка» запускает **side-
|
||
механизм**, а не переход стадии. Если у issue есть **активная** pipeline-задача (queued/running
|
||
job), триггер — **no-op + лог** (не выдёргивать выполняемую работу в Backlog, не трогать
|
||
`STAGE_TRANSITIONS`). Авто-возврат в Backlog **не** создаёт цикла: статус `Backlog` ни одной веткой
|
||
`handle_issue_updated` не обрабатывается (no-op-эхо).
|
||
|
||
### Содержание оценки (сохранено, согласовано с триггером)
|
||
- **BR-1 — Прогноз.** Для задачи оркестратор производит прогноз четырёх величин: **стоимость ($)**,
|
||
**время**, **токены** и **сложность в story points** из фиксированной шкалы `{1,2,3,5,8}`.
|
||
- **BR-2 — База оценки — история.** Прогноз строится на истории похожих **завершённых** задач (по
|
||
типу/стадиям/стеку): средние токены/время/стоимость из уже накопленной фактуры (`agent_runs`,
|
||
`task_usage_summary`, `agent_cost_totals`, тайминги). При отсутствии истории — разумный bootstrap-
|
||
дефолт (не блокирует).
|
||
- **BR-3 — Шкала story points фиксированная** с точной семантикой `1/2/3/5/8` (см. §2). Значение `8` —
|
||
«эпик: разбивать».
|
||
- **BR-4 — On-demand, не блокирующая.** Оценка производится **по запросу** (перевод в «Оценка»), а не
|
||
для каждой задачи автоматически; строго best-effort — сбой/выключение оценки **никогда** не тормозит
|
||
конвейер и не меняет маршрут.
|
||
- **BR-5 — Доступность до старта работы.** Поскольку оператор оценивает задачи на **бэклоге** (до
|
||
`To Analyse`/`start_pipeline`), прогноз доступен **до** перевода задачи в работу — он и нужен для
|
||
планирования отправки задач.
|
||
- **BR-7 — Запись прогноза в Plane.** Прогноз сложности (story points) записывается в поле issue
|
||
**`estimate_point`** (= ОЦЕНКА).
|
||
- **BR-8 — Запись факта в Plane.** По завершении задачи (переход в `done`) фактическая реализованная
|
||
сложность (story points из фактических токенов/времени/стоимости по той же шкале) записывается в
|
||
смежное поле **`point`** — для калибровки; прогноз `estimate_point` при этом **не перезаписывается**.
|
||
- **BR-9 — Отображение на двух поверхностях.** Прогноз публикуется: (a) **Plane-комментом**;
|
||
(b) пунктом **«Оценка»** в общей Telegram-карточке задачи — **время, токены, стоимость**.
|
||
- **BR-10 — Леджер прогноз↔факт (калибровка).** Прогноз и факт сохраняются локально вместе с дельтой
|
||
(ключ `work_item_id`); фундамент петли уточнения модели оценки (связь с ORCH-8). Достаточно
|
||
фиксировать обе величины и дельту (авто-уточнение модели — позже).
|
||
|
||
## 5. Нефункциональные требования (NFR)
|
||
- **NFR-1 — Оценка ≠ Quality Gate / ≠ переход стадии.** Модуль — наблюдатель/продюсер.
|
||
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи (`verdict:`/`result:`/
|
||
`deploy_status:`/`staging_status:`/`security_status:`/`coverage_status:`) / схемы **существующих**
|
||
таблиц — **байт-в-байт не тронуты**. Статус «Оценка» не добавляет ребра в машину стадий; он
|
||
запускает side-механизм и сам возвращает issue в Backlog.
|
||
- **NFR-2 — leaf-паттерн.** never-raise (любой сбой → warning + безопасный дефолт), kill-switch
|
||
`*_enabled`, скоуп `*_repos` (CSV; **пусто → self-hosting only**), read-only блок в `GET /queue`.
|
||
- **NFR-3 — self-hosting safety.** Модуль не рестартит/не роняет прод-контейнер, не трогает `main`/
|
||
force-push, **не вмешивается в горячий путь запуска агентов** (`resolve_agent_model`/
|
||
`resolve_agent_effort`/`_spawn` не модифицируются). Выключенный флаг / неприменимый репо → нулевая
|
||
регрессия для `enduro-trails` и `orchestrator`.
|
||
- **NFR-4 — Стоимость оценки ≪ её ценности.** Сама оценка должна быть дешёвой и быстрой относительно
|
||
выгоды планирования. Выбор механизма (эвристика по истории / отдельный LLM-вызов / гибрид) и баланс
|
||
«точность vs стоимость» — **архитектурное** решение (`06-adr`); в TRZ — лишь требование-ограничение.
|
||
- **NFR-5 — Толерантность к массовости.** Массовый перевод (десятки задач разом → десятки вебхуков
|
||
почти одновременно) **не должен** перегружать прод/конвейер: оценка best-effort, изолирована от
|
||
control-path; механизм сглаживания нагрузки (дешёвая эвристика / очередь / троттлинг) — деталь
|
||
`06-adr`. Требование: bulk не роняет и не тормозит обслуживание других проектов.
|
||
- **NFR-6 — Запись в Plane через существующие примитивы.** `estimate_point`/`point`/коммент/возврат в
|
||
Backlog пишутся через `plane_sync` и подчиняются sandbox write-guard (ORCH-117): в боевом рантайме
|
||
(`uvicorn`) — штатная запись, из тест/worktree-процесса — заблокирована. **Новых секретов/токенов не
|
||
вводится.**
|
||
- **NFR-7 — Fail-safe и инфра-предусловия Plane.** (a) Статус **«Оценка»** должен существовать на
|
||
доске проекта (его отсутствие = fail-closed no-op, BR-T5). (b) estimate-система Plane со значениями
|
||
`1/2/3/5/8` (Fibonacci) для `estimate_point` должна быть настроена; при её отсутствии запись
|
||
`estimate_point`/`point` **best-effort пропускается** (+ лог) и **не роняет** конвейер. Детали и
|
||
точные группы статуса — `07-infra-requirements.md` (архитектор).
|
||
- **NFR-8 — Обратная совместимость данных.** Хранение прогноз↔факт — **аддитивная** новая таблица
|
||
(`CREATE TABLE IF NOT EXISTS`); существующие таблицы/колонки не изменяются.
|
||
|
||
## 6. Допущения и ограничения
|
||
- Оценка на бэклоге работает по **issue** (описание/тип/лейблы из Plane API + история похожих), а не
|
||
по локальной pipeline-задаче: на момент оценки `tasks`-строки может **не быть** → леджер и запись
|
||
ключуются по `work_item_id`, `task_id` — нуллабелен до старта пайплайна.
|
||
- Статус «Оценка» — транзиентный (issue в нём лишь на время оценки, затем Backlog); его Plane-группа
|
||
(`backlog`/`unstarted`) косметична — деталь онбординга/инфры (ORCH-009 расширяется на 23-й статус).
|
||
- Фактура `usage.py`/`agent_runs` достаточна для расчёта факта при завершении; «фактические story
|
||
points» выводятся из факта по той же шкале `{1,2,3,5,8}`.
|
||
- Без ORCH-13 «выбор модели» бессмыслен (один дефолт) — Шаг 2 корректно вынесен в follow-up.
|
||
- Точная Plane-семантика `estimate_point` (FK на estimate-point estimate-системы) vs `point`
|
||
(целочисленный) — деталь реализации/инфры (архитектор + NFR-7).
|
||
|
||
## 7. Критерии успеха
|
||
Заказчик **массово переводит** задачи бэклога в статус **«Оценка»**; по каждой оркестратор
|
||
производит прогноз (стоимость/время/токены/story points), пишет его в `estimate_point`, публикует в
|
||
Plane-комменте и пункте «Оценка» Telegram-карточки, сохраняет в леджер прогноз↔факт и **возвращает
|
||
issue в Backlog**; пере-оценка повтором перевода идемпотентна; по завершении задачи факт пишется в
|
||
`point`. Всё это — без единого изменения control-path/гейтов, без касания горячего пути запуска
|
||
агентов, без выдёргивания in-flight работы; на доске без статуса «Оценка» / при выключенном флаге —
|
||
нулевая регрессия. Детальные PASS/FAIL — `03-acceptance-criteria.md`.
|
||
|
||
## 8. Риски
|
||
- **Статус «Оценка» дёргает in-flight задачу** → снимается BR-T6 (no-op при активном job) + авто-
|
||
возврат только в Backlog, никогда не трогая стадии.
|
||
- **Цикл вебхуков** (возврат в Backlog → новый webhook) → снимается тем, что `Backlog` не
|
||
обрабатывается ни одной веткой `handle_issue_updated` (no-op-эхо) — анти-loop по построению.
|
||
- **Перегрузка от массового перевода** → снимается NFR-5 (best-effort, дешёвый механизм/сглаживание —
|
||
`06-adr`).
|
||
- **Запись в боевой Plane** (`estimate_point`/`point`/коммент/состояние) на общей доске → снимается
|
||
write-guard (ORCH-117) + best-effort/fail-safe (NFR-6/NFR-7).
|
||
- **Неточность прогноза на холодном старте** (мало истории) → bootstrap-дефолт + петля калибровки
|
||
(BR-10).
|
||
- **Расползание в Шаг 2** (control-path) → жёсткий out-of-scope + NFR-3.
|
||
Детальный разбор — `10-tech-risks.md` (архитектор).
|