analyst(ET): auto-commit from analyst run_id=139
All checks were successful
CI / test (push) Successful in 12s

This commit is contained in:
2026-06-06 04:09:41 +00:00
parent 50a3c60b0e
commit c7bca51d4b
4 changed files with 502 additions and 0 deletions

View File

@@ -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/<id>/12-review.md"`.
- **Tester → FAIL** (`check_tests_passed`, ~стр. 455): `task_desc` =
`"…Fix failures described in docs/work-items/<id>/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 не изменилось.

View File

@@ -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`)
Текущее (~стр. 418424):
```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<text>`), а ссылку на файл
оставить как «полный контекст» (`Полный контекст: docs/work-items/<id>/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`)
Текущее (~стр. 454459):
```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<fragment>`), плюс ссылку на файл как полный
контекст.
- Если фрагмент пустой — `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).

View File

@@ -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/<id>/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 отсутствуют.

View File

@@ -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