diff --git a/docs/work-items/ORCH-046/01-brd.md b/docs/work-items/ORCH-046/01-brd.md new file mode 100644 index 0000000..a8070e6 --- /dev/null +++ b/docs/work-items/ORCH-046/01-brd.md @@ -0,0 +1,86 @@ +# BRD — ORCH-046: pass reviewer/tester findings text to developer (not just link) + +Work Item ID: ORCH-046 +Stage: analysis +Author: analyst +Date: 2026-06-06 + +## 1. Контекст и проблема + +Оркестратор при заворотах задачи деву (откат на `development`) формирует +описание задачи (`task_desc`), которое попадает в `.task-dev.md` запускаемого +агента-разработчика. Сейчас в двух ветках отката этот текст содержит **только +ссылку на файл-артефакт**, без сути замечаний: + +- **Reviewer → REQUEST_CHANGES** (`src/stage_engine.py`, ветка + `_handle_qg_failure_rollbacks`, ~стр. 419): `task_desc` = + `"…Fix findings in docs/work-items//12-review.md"`. +- **Tester → FAIL** (`check_tests_passed`, ~стр. 455): `task_desc` = + `"…Fix failures described in docs/work-items//13-test-report.md"`. + +В результате developer-агент получает инструкцию «иди читай файл». Ключевые +претензии (P0/P1 у ревьюера, причина падения у тестера) часто проскакивают — +агент не открывает файл целиком или теряет фокус, повторяет ту же ошибку, и +задача снова заворачивается. Это «испорченный телефон»: расход циклов retry +(`MAX_DEVELOPER_RETRIES = 3`), деньги на токены, простой конвейера. + +## 2. Бизнес-цель + +Убрать «испорченный телефон» между reviewer/tester и developer при заворотах: +встраивать **дословный текст ключевых замечаний** прямо в `task_desc`, чтобы +developer-агент видел суть претензий сразу, а не только ссылку. + +Это снижает число повторных заворотов и расход retry-бюджета на одну задачу. + +## 3. Объём (вариант A — выбран Славой 06.06) + +Минимальное, низкорисковое изменение **ядра** (`stage_engine`), которое: + +1. Извлекает из `12-review.md` блок findings и выносит **must-fix (P0/P1) + дословно** в `task_desc` при reviewer REQUEST_CHANGES. +2. Извлекает из `13-test-report.md` причину FAIL (reason из гейта + релевантный + фрагмент тела отчёта) в `task_desc` при tester FAIL. +3. Во всех случаях **сохраняет ссылку на полный файл** как дополнительный + контекст («полный контекст — см. файл»). +4. Извлечение выполняется новым отдельным хелпером-парсером + (`src/review_parse.py`), который **никогда не бросает исключение**: при + отсутствующем/битом файле возвращает пустой результат, и вызывающий код + делает graceful fallback на прежнюю ссылку-строку. + +## 4. Что НЕ входит в объём (out of scope) + +- НЕ трогать гейты `check_*` (в т. ч. ORCH-45 `check_ci_green`, + ORCH-47 `_parse_tests_verdict`) — они в проде, поведение неизменно. +- НЕ трогать реестр `QG_CHECKS`. +- НЕ менять сигнатуры публичных функций (`advance_stage`, `_run_qg`, + `check_*`). +- НЕ менять webhook-пути. +- НЕ менять retry-счётчик (`_developer_retry_count`, `MAX_DEVELOPER_RETRIES`) + и rollback-логику (последовательность `update_task_stage` → + `notify_stage_change` → `plane_notify_stage` → enqueue) — поведение + идентично. +- НЕ менять формат Plane-комментариев (`build_status_comment`). + +## 5. Заинтересованные стороны + +- **Owner (Слава)** — заказчик, выбрал вариант A. +- **Developer-агенты** — потребители `task_desc`: получают суть замечаний. +- **Конвейер всех проектов** (self-hosting) — выигрывает за счёт меньшего + числа заворотов. + +## 6. Ограничения и риски (self-hosting) + +- Правка ядра `stage_engine` — компонент крутится в продакшене и обслуживает + все проекты из общего инстанса/БД/очереди. Любая регрессия в формировании + `task_desc` или (тем более) исключение в `advance_stage` останавливает + конвейер всех проектов → **парсер обязан быть полностью graceful**. +- Обязателен прогон `deploy-staging` (8501) перед прод-деплоем. +- Это правка ядра → требуется ADR (per-work-item). + +## 7. Критерий успеха (бизнес) + +- При заворотах в `.task-dev.md` есть дословный текст ключевых замечаний + (P0/P1 ревьюера; reason+фрагмент тестера) плюс ссылка на полный файл. +- Парсер устойчив к битым/отсутствующим артефактам (graceful fallback на + старую ссылку-строку). +- Существующие тесты зелёные; поведение retry/rollback не изменилось. diff --git a/docs/work-items/ORCH-046/02-trz.md b/docs/work-items/ORCH-046/02-trz.md new file mode 100644 index 0000000..b6d6340 --- /dev/null +++ b/docs/work-items/ORCH-046/02-trz.md @@ -0,0 +1,209 @@ +# ТЗ — ORCH-046: встраивание текста findings reviewer/tester в task_desc + +Work Item ID: ORCH-046 +Stage: analysis +Author: analyst +Date: 2026-06-06 + +> Вариант A (минимальный, низкий риск). Это правка ЯДРА — обязателен ADR +> (per-work-item, `docs/work-items/ORCH-046/06-adr/`). + +## 1. Задействованные модули `src/` + +| Модуль | Изменение | +|--------|-----------| +| `src/review_parse.py` | **НОВЫЙ** хелпер-парсер: `extract_review_findings(path) -> str`, `extract_test_failures(path) -> str`. | +| `src/stage_engine.py` | Две ветки в `_handle_qg_failure_rollbacks`: reviewer REQUEST_CHANGES (~стр. 419) и tester `check_tests_passed` FAIL (~стр. 455) — встраивают извлечённый текст в `task_desc`. | + +Источники-образцы (не менять, использовать как референс паттерна «never raise» и +формата артефактов): +- `src/qg/checks.py::_parse_tests_verdict` — образец «never raise», split по `---`, `yaml.safe_load`. +- `src/frontmatter.py::read_frontmatter_value` — образец defensive-парсера. +- `.openclaw/agents/reviewer.md` — канонический формат `12-review.md`. +- `.openclaw/agents/tester.md` — канонический формат `13-test-report.md`. + +## 2. Новый модуль `src/review_parse.py` + +### 2.1. `extract_review_findings(path: str) -> str` + +Назначение: вернуть **дословный** текст must-fix findings (P0 + P1) из +`12-review.md` для встраивания в `task_desc`. + +Формат входного файла (канон reviewer.md, секция `## Findings`): + +```markdown +## Findings + +### P0 — Blocker +- [ ] <описание> + +### P1 — Must fix +- [ ] <описание> + +### P2 — Should fix +- [ ] <описание> +``` + +Требования к реализации: + +1. **Никогда не бросает исключение.** Любая ошибка (нет файла, IOError, кривой + markdown, нет секции `## Findings`) → возврат `""` (пустая строка). +2. Парсит **только** подсекции P0 и P1 (must-fix). P2/P3 игнорируются. +3. Заголовки подсекций распознаются устойчиво к регистру и к тире/дефису: + соответствие по наличию токена `P0` / `P1` в строке-заголовке уровня `###`. +4. Из распознанных подсекций берётся текст до следующего заголовка `###`/`##` + (т. е. тело подсекции дословно: пункты списка `- [ ] …` / `- …`). +5. Пустые подсекции (нет содержательных пунктов, только `(если есть)`-плейсхолдер + или ничего) — пропускаются. Если ни одного содержательного P0/P1 пункта нет + → возврат `""`. +6. Результат — компактный многострочный текст, пригодный для вставки в + `task_desc` (например, заголовок подсекции + её пункты). Длина результата + ограничивается разумным лимитом (`MAX_FINDINGS_CHARS`, напр. 2000) с + усечением и маркером `…(truncated)`; полный контекст всё равно остаётся в + файле. +7. Frontmatter (верхний `--- … ---`) при необходимости отбрасывается, чтобы не + попасть в тело; парсинг секции делается по телу markdown. + +Сигнатура и контракт (стабильны): +```python +def extract_review_findings(path: str) -> str: + """Дословный текст P0/P1 findings из 12-review.md. Never raises; '' при ошибке/пусто.""" +``` + +### 2.2. `extract_test_failures(path: str) -> str` + +Назначение: вернуть текст причины падения тестов из `13-test-report.md` для +встраивания в `task_desc`. + +Формат входного файла (канон tester.md): frontmatter `result: PASS|FAIL`, далее +тело с секциями `## Результаты` (таблица TC), `## Вывод pytest`, `## Итог`. + +Требования к реализации: + +1. **Никогда не бросает исключение.** Любая ошибка → возврат `""`. +2. Извлекает релевантный фрагмент тела, помогающий понять причину FAIL. + Приоритет источников (берём первый непустой): + - секция `## Вывод pytest` (вывод прогона — где видно упавшие тесты), и/или + - строки таблицы `## Результаты`, содержащие `FAIL`, и/или + - секция `## Итог`. +3. Результат усекается до `MAX_FAILURES_CHARS` (напр. 2000) с маркером + `…(truncated)`. +4. Если ничего извлечь не удалось → возврат `""` (вызывающий код делает + fallback на ссылку). + +> Примечание: «reason» из самого гейта (`check_tests_passed` → второй элемент +> кортежа) у вызывающего кода уже есть (`reason`) — он добавляется в `task_desc` +> вызывающим кодом (как и сейчас в комментарии тестера). `extract_test_failures` +> добавляет **фрагмент тела отчёта** поверх этого reason. + +Сигнатура и контракт (стабильны): +```python +def extract_test_failures(path: str) -> str: + """Релевантный фрагмент тела 13-test-report.md (причина FAIL). Never raises; '' при ошибке/пусто.""" +``` + +### 2.3. Общие требования модуля + +- Модуль логирует диагностические сообщения на уровне `logger.debug` + (`logging.getLogger("orchestrator.review_parse")`), как `frontmatter.py`. +- Никаких сетевых вызовов, только чтение файла с диска. +- Константы лимитов вынесены модульными (`MAX_FINDINGS_CHARS`, + `MAX_FAILURES_CHARS`). + +## 3. Изменения `src/stage_engine.py` + +### 3.1. Ветка reviewer REQUEST_CHANGES (внутри `_handle_qg_failure_rollbacks`) + +Текущее (~стр. 418–424): +```python +task_desc = ( + f"Work item: {work_item_id}\nRepo: {repo}\nBranch: {branch}\n" + f"Stage: development\nNote: REQUEST_CHANGES from reviewer " + f"(attempt {retry_count+1}/3). Fix findings in " + f"docs/work-items/{work_item_id}/12-review.md" +) +``` + +Целевое поведение: +- Сформировать путь к `12-review.md` через `get_worktree_path(repo, branch)` + + `docs/work-items/{work_item_id}/12-review.md` (как в `_check_review_approved_by_branch`). +- Вызвать `extract_review_findings(path)`. +- Если результат непустой — встроить findings **дословно** в `task_desc` + (под подзаголовком, напр. `Findings (P0/P1):\n`), а ссылку на файл + оставить как «полный контекст» (`Полный контекст: docs/work-items//12-review.md`). +- Если результат пустой (graceful fallback) — `task_desc` остаётся **как + сейчас** (ссылка-строка). Никаких исключений. +- Префиксная часть (`Work item / Repo / Branch / Stage / Note: REQUEST_CHANGES … + (attempt N/3)`) сохраняется без изменений. + +### 3.2. Ветка tester FAIL (`check_tests_passed`, внутри `_handle_qg_failure_rollbacks`) + +Текущее (~стр. 454–459): +```python +task_desc = ( + f"Work item: {work_item_id}\nRepo: {repo}\nBranch: {branch}\n" + f"Stage: development\nNote: Tests FAILED. " + f"Fix failures described in docs/work-items/{work_item_id}/13-test-report.md" +) +``` + +Целевое поведение: +- Сформировать путь к `13-test-report.md` аналогично. +- Вызвать `extract_test_failures(path)`. +- В `task_desc` всегда включить `reason` (он уже доступен в этой ветке — + передаётся в `_handle_qg_failure_rollbacks`). +- Если фрагмент тела непустой — встроить его дословно + (`Причина: {reason}\nДетали:\n`), плюс ссылку на файл как полный + контекст. +- Если фрагмент пустой — `task_desc` содержит `reason` + ссылку (graceful + fallback, не хуже текущего поведения). Никаких исключений. +- Префиксная часть и существующий Plane-комментарий тестера + (`❌ Тесты не прошли: {reason}…`) НЕ меняются. + +### 3.3. Инварианты (НЕ менять поведение) + +- Последовательность rollback в обеих ветках: `update_task_stage(task_id, + "development")` → `notify_stage_change` → `plane_notify_stage` → + (`set_issue_in_progress` для тестера) → проверка `_developer_retry_count` < + `MAX_DEVELOPER_RETRIES` → `enqueue_job("developer", …)` либо + `send_telegram` alert. Порядок и условия идентичны. +- `result.rolled_back_to`, `result.enqueued_agent`, `result.enqueued_job_id`, + `result.alerted` выставляются как сейчас. +- Меняется **только содержимое строки `task_desc`**, передаваемой в + `enqueue_job`. +- Импорт нового модуля — `from .review_parse import extract_review_findings, + extract_test_failures` в шапке `stage_engine.py`. + +## 4. Изменения API + +Нет. Публичные HTTP-эндпоинты (`/health`, `/status`, `/queue`, +`/webhook/plane`, `/webhook/gitea`) не затрагиваются. + +## 5. Изменения схемы БД + +Нет. Таблицы `tasks`, `agent_runs`, `jobs`, `events` не меняются. +`enqueue_job` вызывается с прежней сигнатурой. + +## 6. Требования к новым QG checks + +Нет. Реестр `QG_CHECKS` и все `check_*` не трогаются (явно out of scope). + +## 7. Артефакты pipeline (создать/обновить в этом PR) + +- `src/review_parse.py` — новый модуль. +- `tests/test_review_parse.py` — юнит-тесты парсера (см. 04-test-plan.yaml). +- Возможные дополнения в `tests/test_stage_engine.py` — проверка встраивания + текста в `task_desc` (rollback-ветки). +- `docs/work-items/ORCH-046/06-adr/ADR-001-*.md` — ADR (правка ядра). +- `docs/architecture/README.md` / `internals.md` — описание нового хелпера и + поведения заворотов (если reviewer сочтёт необходимым; компонент описать в + разделе Stage Engine / Откаты). +- `CHANGELOG.md` — запись о ORCH-046. + +## 8. Контроль качества / проверка + +```bash +python -m pytest tests/ -q # в контейнере; все тесты зелёные +``` + +Обязательно: стадия `deploy-staging` (8501) перед прод-деплоем (self-hosting). diff --git a/docs/work-items/ORCH-046/03-acceptance-criteria.md b/docs/work-items/ORCH-046/03-acceptance-criteria.md new file mode 100644 index 0000000..4882b4d --- /dev/null +++ b/docs/work-items/ORCH-046/03-acceptance-criteria.md @@ -0,0 +1,99 @@ +# Критерии приёмки — ORCH-046 + +Work Item ID: ORCH-046 +Stage: analysis +Author: analyst +Date: 2026-06-06 + +Каждый критерий имеет чёткое условие PASS/FAIL. Reviewer/Tester проверяют по +этому списку. + +## AC-1 — Дословные P0/P1 findings ревьюера в task_desc + +**Условие:** при reviewer REQUEST_CHANGES (откат `review`/`testing` → +`development`) строка `task_desc`, переданная в `enqueue_job("developer", …)`, +содержит ДОСЛОВНЫЙ текст findings уровня P0/P1 из `12-review.md` (не только +ссылку). + +- **PASS:** в `task_desc` присутствуют дословные строки P0/P1 пунктов из секции + `## Findings` файла `12-review.md`. +- **FAIL:** `task_desc` содержит только ссылку на файл, без текста findings (при + наличии валидного файла с P0/P1). + +## AC-2 — Причина падения тестера в task_desc + +**Условие:** при tester FAIL (`check_tests_passed`, откат `testing` → +`development`) строка `task_desc` содержит причину падения: `reason` из гейта + +релевантный фрагмент тела `13-test-report.md`. + +- **PASS:** `task_desc` содержит `reason` И непустой фрагмент тела отчёта + (вывод pytest / FAIL-строки / Итог), когда отчёт валиден. +- **FAIL:** `task_desc` содержит только ссылку на файл без причины/фрагмента + (при наличии валидного отчёта). + +## AC-3 — Ссылка на полный файл сохранена + +**Условие:** в обеих ветках (reviewer, tester) `task_desc` по-прежнему содержит +ссылку на полный файл-артефакт (`docs/work-items//12-review.md` / +`13-test-report.md`) как дополнительный контекст. + +- **PASS:** путь к файлу присутствует в `task_desc` в обоих сценариях. +- **FAIL:** ссылка на файл удалена/отсутствует. + +## AC-4 — Парсер устойчив к отсутствию/битому файлу (graceful) + +**Условие:** `extract_review_findings(path)` и `extract_test_failures(path)` +НИКОГДА не бросают исключение; при отсутствующем/нечитаемом/битом файле +возвращают `""`, а вызывающий код в `stage_engine` делает fallback на прежнюю +ссылку-строку. + +- **PASS:** на несуществующем пути, пустом файле, файле без секций, битом + markdown/YAML — функции возвращают `""` без исключения; `advance_stage` + отрабатывает откат как раньше (ссылка-строка в `task_desc`). +- **FAIL:** любое исключение наружу из парсера или из `advance_stage` из-за + парсинга. + +## AC-5 — Тесты зелёные + новые юнит-тесты парсера + +**Условие:** существующие тесты не сломаны; добавлены юнит-тесты парсера, +покрывающие: findings есть / findings пусто / битый YAML(frontmatter) / только +P3 (нет P0/P1). + +- **PASS:** `python -m pytest tests/ -q` зелёный; `tests/test_review_parse.py` + содержит как минимум кейсы: P0/P1 присутствуют → текст возвращён; нет + findings/только P2-P3 → `""`; битый файл → `""`; отсутствующий путь → `""`; + для test-report: FAIL-фрагмент извлечён / пустой отчёт → `""`. +- **FAIL:** падение существующих тестов или отсутствие перечисленных кейсов. + +## AC-6 — Retry-счётчик и rollback НЕ изменены по поведению + +**Условие:** логика `_developer_retry_count`, `MAX_DEVELOPER_RETRIES = 3`, +последовательность откатов и поля `AdvanceResult` (`rolled_back_to`, +`enqueued_agent`, `enqueued_job_id`, `alerted`) идентичны прежним. + +- **PASS:** существующие тесты `test_stage_engine.py` на rollback/retry зелёные; + при 4-м заходе по-прежнему alert вместо enqueue; меняется только текст + `task_desc`. +- **FAIL:** изменилось число retry, порядок вызовов, или значения полей + `AdvanceResult`. + +## AC-7 — Out-of-scope не затронут + +**Условие:** не изменены: `check_*` гейты, реестр `QG_CHECKS`, сигнатуры +публичных функций (`advance_stage`, `_run_qg`, `check_*`), webhook-пути, формат +Plane-комментариев. + +- **PASS:** `git diff` не содержит изменений в `src/qg/checks.py` (логика + гейтов), сигнатурах публичных функций, `src/webhooks/*`, + `usage.build_status_comment`; `test_qg_registry_snapshot` зелёный. +- **FAIL:** любое из перечисленного изменено. + +## AC-8 — Документация и ADR обновлены (golden source) + +**Условие:** правка ядра → заведён ADR (`06-adr/`), обновлён `CHANGELOG.md`, при +необходимости — `docs/architecture/README.md`/`internals.md` (раздел Stage +Engine / Откаты). + +- **PASS:** присутствует `docs/work-items/ORCH-046/06-adr/ADR-001-*.md`; в + `CHANGELOG.md` есть запись ORCH-046. +- **FAIL:** ADR или запись в CHANGELOG отсутствуют. diff --git a/docs/work-items/ORCH-046/04-test-plan.yaml b/docs/work-items/ORCH-046/04-test-plan.yaml new file mode 100644 index 0000000..9a01980 --- /dev/null +++ b/docs/work-items/ORCH-046/04-test-plan.yaml @@ -0,0 +1,108 @@ +work_item: ORCH-046 +description: > + Тест-план для встраивания дословного текста findings reviewer/tester в + task_desc при заворотах деву. Покрывает новый парсер src/review_parse.py + (graceful, never-raise) и две rollback-ветки src/stage_engine.py. + +tests: + # --- Парсер review findings (extract_review_findings) ------------------- + - id: TC-01 + type: unit + description: "extract_review_findings возвращает дословный текст P0/P1 при их наличии в 12-review.md" + module: tests/test_review_parse.py + covers: [AC-1, AC-5] + expected: PASS + + - id: TC-02 + type: unit + description: "extract_review_findings возвращает '' когда есть только P2/P3 (нет must-fix P0/P1)" + module: tests/test_review_parse.py + covers: [AC-5] + expected: PASS + + - id: TC-03 + type: unit + description: "extract_review_findings возвращает '' для отсутствующего файла (несуществующий путь), без исключения" + module: tests/test_review_parse.py + covers: [AC-4] + expected: PASS + + - id: TC-04 + type: unit + description: "extract_review_findings возвращает '' для битого/пустого файла или markdown без секции ## Findings, без исключения" + module: tests/test_review_parse.py + covers: [AC-4, AC-5] + expected: PASS + + - id: TC-05 + type: unit + description: "extract_review_findings усекает очень длинные findings до лимита с маркером truncated" + module: tests/test_review_parse.py + covers: [AC-1] + expected: PASS + + # --- Парсер test failures (extract_test_failures) ---------------------- + - id: TC-06 + type: unit + description: "extract_test_failures извлекает релевантный фрагмент тела (Вывод pytest / FAIL-строки / Итог) из 13-test-report.md с result: FAIL" + module: tests/test_review_parse.py + covers: [AC-2, AC-5] + expected: PASS + + - id: TC-07 + type: unit + description: "extract_test_failures возвращает '' для отсутствующего файла, без исключения" + module: tests/test_review_parse.py + covers: [AC-4] + expected: PASS + + - id: TC-08 + type: unit + description: "extract_test_failures возвращает '' для битого/пустого отчёта (нет тела/секций), без исключения" + module: tests/test_review_parse.py + covers: [AC-4, AC-5] + expected: PASS + + # --- Интеграция со stage_engine (rollback task_desc) ------------------- + - id: TC-09 + type: integration + description: "advance_stage: reviewer REQUEST_CHANGES -> в enqueue_job('developer') task_desc содержит дословные P0/P1 findings И ссылку на 12-review.md" + module: tests/test_stage_engine.py + covers: [AC-1, AC-3] + expected: PASS + + - id: TC-10 + type: integration + description: "advance_stage: tester check_tests_passed FAIL -> task_desc содержит reason + фрагмент 13-test-report.md И ссылку на файл" + module: tests/test_stage_engine.py + covers: [AC-2, AC-3] + expected: PASS + + - id: TC-11 + type: integration + description: "advance_stage: reviewer REQUEST_CHANGES при отсутствующем/битом 12-review.md -> graceful fallback, task_desc = прежняя ссылка-строка, без исключения" + module: tests/test_stage_engine.py + covers: [AC-4, AC-3] + expected: PASS + + - id: TC-12 + type: integration + description: "advance_stage: rollback/retry поведение неизменно — последовательность откатов, _developer_retry_count, alert на 4-й заход, поля AdvanceResult" + module: tests/test_stage_engine.py + covers: [AC-6] + expected: PASS + + # --- Регресс / неизменность out-of-scope ------------------------------ + - id: TC-13 + type: integration + description: "Реестр QG_CHECKS не изменён (snapshot), гейты check_* нетронуты" + module: tests/test_qg_registry_snapshot.py + covers: [AC-7] + expected: PASS + + - id: TC-14 + type: integration + description: "Полный регресс существующего набора зелёный: python -m pytest tests/ -q" + module: tests/ + covers: [AC-5, AC-6, AC-7] + expected: PASS