diff --git a/tasks/orchestrator/DEV_TASK_DEPLOY_VERDICT.md b/tasks/orchestrator/DEV_TASK_DEPLOY_VERDICT.md new file mode 100644 index 0000000..df725b3 --- /dev/null +++ b/tasks/orchestrator/DEV_TASK_DEPLOY_VERDICT.md @@ -0,0 +1,95 @@ +# DEV TASK — БАГ 8: deploy не имеет QG-гейта → провалившийся деплой уходит в `done` + +**Проект:** orchestrator | **Сервер:** slin@82.22.50.71 | **Репо:** /home/slin/repos/orchestrator | **Контейнер:** orchestrator (8500) +**Ветка:** `fix/deploy-verdict-gate` из свежего main (`git checkout main && git pull && git checkout -b fix/deploy-verdict-gate`). main = `a0621b9` (PR #18, перед стартом сделай `git pull` — мог уехать). + +⚠️ **ГРАБЛЯ push (ORCH-7):** после push `git log origin/main..origin/fix/deploy-verdict-gate` ДОЛЖЕН показать коммит ДО отчёта «PR готов». Токен Gitea: `docker exec orchestrator printenv ORCH_GITEA_TOKEN`. + +## Контекст / дыра (баг 8, подтверждён боевым прогоном ET-011) +deployer (run 71) РЕАЛЬНО провалил деплой (deploy-hook упал на permission denied, контейнер не пересобрался), честно написал `Status: FAILED` в `14-deploy-log.md` и в текстовом отчёте «Exiting with non-zero». **НО задача всё равно уехала в `done`**, и фича оказалась нерабочей на проде. + +**Корень — ДВА места:** +1. `src/stages.py:19` — у стадии deploy `"qg": None`. **Нет проверки выхода вообще** (у всех других стадий QG есть). deploy → done безусловно. +2. `exit_code` в `agent_runs` = код процесса LLM-агента (Claude CLI), который ВСЕГДА 0 при успешной LLM-сессии — не отражает реальный результат деплоя. Существующая защита `launcher.py:475 if exit_code != 0 and agent == "deployer"` поэтому НЕ срабатывает. + +**Решение (по образцу check_reviewer_verdict, S-5):** завести QG `check_deploy_status`, который читает машинно-читаемое поле из `14-deploy-log.md` (как reviewer читает `verdict:` из `12-review.md`). Привязать к стадии deploy. deployer-промпт обязать писать это поле во frontmatter. + +## Правка 1 — новый QG `check_deploy_status` в `src/qg/checks.py` +Добавь функцию ПО ОБРАЗЦУ `check_reviewer_verdict` (checks.py:210). Образец читает frontmatter `verdict:` из `12-review.md`. Сделай аналог для `14-deploy-log.md`, поле `deploy_status:`: +```python +def check_deploy_status(repo: str, work_item_id: str, branch: str | None = None) -> tuple[bool, str]: + """ + БАГ 8 fix: gate the deploy -> done transition on the deployer's machine-readable + verdict in 14-deploy-log.md frontmatter, NOT on the LLM process exit code + (which is always 0 on a successful agent session even when the deploy failed). + + Mirrors check_reviewer_verdict (S-5): reads ONLY `deploy_status:` from YAML + frontmatter. Returns: + (True, ...) -> deploy_status: SUCCESS + (False, ...) -> deploy_status: FAILED, missing field, or no frontmatter + """ + import yaml + repo_path = _repo_path(repo, branch) + log_path = os.path.join(repo_path, f"docs/work-items/{work_item_id}/14-deploy-log.md") + + if not os.path.isfile(log_path): + return False, "Deploy log not found (14-deploy-log.md)" + try: + with open(log_path, "r") as f: + content = f.read() + status = None + if content.startswith("---"): + parts = content.split("---", 2) + if len(parts) >= 3: + try: + fm = yaml.safe_load(parts[1]) or {} + except yaml.YAMLError as e: + return False, f"Invalid YAML frontmatter in deploy log: {e}" + status = str(fm.get("deploy_status", "")).upper().strip() + if status == "SUCCESS": + return True, "Deploy status: SUCCESS" + if status == "FAILED": + return False, "Deploy status: FAILED" + return False, f"No machine-readable deploy_status in frontmatter (got: {status!r})" + except OSError as e: + return False, f"Error reading deploy log: {e}" +``` +И зарегистрируй в `QG_CHECKS` (рядом с остальными, ~стр.286): `"check_deploy_status": check_deploy_status,`. + +## Правка 2 — привязать QG к стадии deploy в `src/stages.py:19` +БЫЛО: `"deploy": {"next": "done", "agent": None, "qg": None},` +СТАЛО: `"deploy": {"next": "done", "agent": None, "qg": "check_deploy_status"},` +(agent остаётся None — deployer запускается на ВХОДЕ стадии deploy из testing, как сейчас; check_deploy_status гейтит ВЫХОД deploy→done.) + +## Правка 3 — где вызывается advance deploy→done: убедиться, что QG check_deploy_status проверяется +Найди в `src/stage_engine.py` / `src/agents/launcher.py`, где после deployer'а делается переход `deploy → done` (auto-advance). Сейчас он, видимо, безусловный (qg=None). Нужно: ПЕРЕД `update_task_stage(task_id, "done")` вызвать `check_deploy_status(repo, work_item_id, branch)`: +- если **True** → done (как сейчас) + notify; +- если **False** → НЕ done. Вместо этого: `update_task_stage(task_id, "development")` + `set_issue_blocked(work_item_id)` + `notify_qg_failure(task_id, "deploy", "check_deploy_status", reason)` + Telegram «Deploy FAILED, rolled back to development». (Это дублирует существующий launcher.py:475-495 блок deployer-failure — можно переиспользовать ту же логику, главное чтобы она реально вызывалась на основе ВЕРДИКТА, а не exit_code.) +- ⚠️ Точное место advance найди сам (grep `deploy.*done`, `update_task_stage.*done`, `next_stage == "done"`, `_try_advance`). НЕ ломай advance для других стадий. + +## Правка 4 — deployer-промпт `.openclaw/agents/deployer.md` (в репо enduro-trails, НЕ в orchestrator!) +⚠️ Этот файл — в целевом репо `admin/enduro-trails`, ветка отдельная. **СНАЧАЛА уточни у ассистента** нужно ли его трогать в этой задаче или отдельным PR в enduro-trails. Если ассистент скажет править здесь — добавь в инструкцию deployer'а требование писать YAML-frontmatter в начало `14-deploy-log.md`: +``` +--- +deploy_status: SUCCESS # или FAILED +version: v0.0.3 +--- +``` +**По умолчанию (если ассистент не уточнил) — НЕ трогай enduro-trails, только orchestrator. Отметь в отчёте, что deployer-промпт enduro-trails требует отдельной правки (добавить frontmatter deploy_status).** + +## Ограничения +- 🚫 НЕ трогай: другие QG-функции (check_ci_green, check_reviewer_verdict, check_tests_passed, check_tests_local, check_architecture_done, check_analysis_*), review/CI-retry ветки, HMAC/project-filter, nginx, openclaw.json, .env, очередь, PLANE_STATES, conftest.py, status-only, gitea_public_url, стадии кроме deploy. +- 🚫 НЕ удаляй существующий launcher.py:475 блок (можешь переиспользовать его логику для вердикт-пути). +- Conventional Commits, один коммит: `fix(deploy): gate deploy->done on deployer verdict, not LLM exit code`. + +## Тесты (контейнер) +`IMG=$(docker inspect orchestrator --format '{{.Config.Image}}'); docker run --rm -v /home/slin/repos/orchestrator:/code -w /code --entrypoint python3 $IMG -m pytest tests/ -q` +Добавь в `tests/test_qg.py` + `tests/test_stage_engine.py` (pure-logic, БЕЗ TestClient POST — иначе 401 HMAC baseline): +- `check_deploy_status`: SUCCESS frontmatter → (True,...); FAILED → (False,...); нет файла → (False,...); нет поля → (False,...). (создай tmp 14-deploy-log.md с фронтматтером, мокни _repo_path/путь). +- `get_qg_for_stage("deploy") == "check_deploy_status"`. +- advance: deploy с deploy_status=FAILED → задача НЕ в done (откат в development), deploy_status=SUCCESS → done. +- Baseline после PR #18: **217 passed + 10 failed** (10 off-limits: 9 HMAC/401 + 1 webhook-POST). Итог passed ≥217 + новые. НЕ урони passed. + +## Отчёт +- НЕ деплоить, НЕ мержить (мерж + боевой прогон делает ассистент). +- В отчёте: точный код check_deploy_status, правка stages.py, ГДЕ именно нашёл advance deploy→done и как туда вписал вердикт-проверку, новые тесты, полный вывод pytest. Отметь статус правки 4 (deployer-промпт enduro-trails). Один исполнитель.