Merge pull request 'fix(analysis): activate analyst open-questions -> Needs Input flow (ORCH-120)' (#146) from feature/ORCH-120-bug-analyst-open-questions-mus into main
This commit was merged in pull request #146.
This commit is contained in:
13
.env.example
13
.env.example
@@ -239,6 +239,19 @@ ORCH_SERIAL_GATE_ENABLED=true
|
||||
ORCH_SERIAL_GATE_REPOS=
|
||||
ORCH_SERIAL_GATE_FREEZE_ENABLED=true
|
||||
ORCH_SERIAL_GATE_PAUSE_ENABLED=true
|
||||
# ORCH-120 (adr-0053): analyst open-questions -> Needs Input. Activates the dead
|
||||
# "analyst asks BLOCKING questions -> 01-questions.md -> Needs Input" path in
|
||||
# _handle_analysis_approved_flow. Additive, never-raise, self-hosting scope;
|
||||
# STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict / DB schema UNCHANGED.
|
||||
# ANALYST_QUESTIONS_GATE_ENABLED=false -> _handle_analysis_approved_flow runs its
|
||||
# ORIGINAL pre-ORCH-120 order (files_ok first, then flat isfile check) byte-for-byte.
|
||||
# ANALYST_QUESTIONS_GATE_REPOS (CSV) -> scope; EMPTY = self-hosting only (orchestrator).
|
||||
# ANALYST_NEEDS_INPUT_AUTOPAUSE_ENABLED=true (default) -> auto-park a Needs-Input task
|
||||
# (db.set_task_paused) so the repo serial-gate FIFO does not wedge while we wait for a
|
||||
# human; unpark on resume. false -> operator-park only (POST /serial-gate/pause).
|
||||
ORCH_ANALYST_QUESTIONS_GATE_ENABLED=true
|
||||
ORCH_ANALYST_QUESTIONS_GATE_REPOS=
|
||||
ORCH_ANALYST_NEEDS_INPUT_AUTOPAUSE_ENABLED=true
|
||||
# ORCH-090: STOP-status task cancellation (stop active agent + full progress reset)
|
||||
# and the relaunch-hole close. A dedicated Plane "STOP" status (logical key `stop`,
|
||||
# fail-closed: absent from _DEFAULT_STATES, so a board without the status -> no-op)
|
||||
|
||||
@@ -40,6 +40,21 @@ bug-report (симптом / шаги воспроизведения / лока
|
||||
**сложным/архитектурным/визуальным** (нужен ADR или макет) — выпусти **полный** analysis-пакет и
|
||||
помечай в bug-report `escalate: full-cycle` (эскалация в полный цикл, ADR-001 D5 ORCH-019); оператор
|
||||
снимает багфикс-трек эндпоинтом `POST /bug-fast-track/escalate`.
|
||||
|
||||
**Блокирующие вопросы → Needs Input (ORCH-120, adr-0053).** Если бизнес-запрос **блокирующе**
|
||||
неоднозначен и выпустить корректные 4 deliverables нельзя без ответа заказчика — **НЕ фабрикуй**
|
||||
требования ради сдачи файлов. Вместо этого через **Write tool** запиши
|
||||
`docs/work-items/<plane-id>/01-questions.md` (скелет — `docs/_templates/01-questions.md`) со списком
|
||||
**конкретных** блокирующих вопросов (с вариантами и тем, что разблокирует анализ). Наличие активных
|
||||
вопросов уводит задачу в **Needs Input** (движок `_handle_analysis_approved_flow` ставит статус +
|
||||
комментирует вопросы в Plane) — **приоритетно** над «файлы готовы». Это сигнальный артефакт (гейтом
|
||||
не парсится), пиши его ТОЛЬКО при реальных блокерах.
|
||||
|
||||
**Поведение на перезапуске (resume).** После ответа заказчика в Plane тебя перезапускают: прочитай
|
||||
**свежие комментарии-ответы**, затем (а) если все блокеры сняты — выпусти **полный** валидный пакет
|
||||
(4 файла); свежий пакет автоматически **supersede’ит** старый `01-questions.md` по mtime (повторного
|
||||
Needs Input не будет); (б) если часть вопросов осталась — **перепиши** `01-questions.md`, оставив
|
||||
только актуальные блокеры (снова Needs Input). Не оставляй устаревшие вопросы вперемешку с новыми.
|
||||
</task>
|
||||
|
||||
<deliverables>
|
||||
@@ -52,6 +67,10 @@ bug-report (симптом / шаги воспроизведения / лока
|
||||
| `03-acceptance-criteria.md` | Критерии приёмки (чёткие условия PASS/FAIL) |
|
||||
| `04-test-plan.yaml` | План тестов (unit, integration; pytest) |
|
||||
|
||||
**When-applicable (сигнальный, ORCH-120):** `01-questions.md` — пишется **только** при блокирующих
|
||||
открытых вопросах (см. `<task>`) **вместо** сфабрикованных 4 файлов; скелет —
|
||||
`docs/_templates/01-questions.md`. Не machine-verdict, гейтом не парсится.
|
||||
|
||||
**Скелеты:** бери из `docs/_templates/` (одноимённые файлы) — не угадывай структуру.
|
||||
**Эталон качества/полноты:** заполненные work item **ORCH-088** и **ORCH-073** —
|
||||
ориентируйся на их детальность и формат.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Формат: [Keep a Changelog](https://keepachangelog.com/). Записи — на смысловой PR/задачу.
|
||||
|
||||
## [Unreleased]
|
||||
- **Открытые вопросы аналитика → Needs Input (приоритет, неблокирование serial-gate, resume)** (ORCH-120, `fix`, трек Bug→escalate full-cycle): активирован и достроен ранее **мёртвый** путь «аналитик задаёт блокирующие вопросы → `01-questions.md` → Needs Input». Четыре согласованных изменения, аддитивно, под kill-switch, скоуп self-hosting, never-raise; `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` / machine-verdict-ключи / схема БД — **байт-в-байт не тронуты** (поток — pre-gate-ветка движка, **не** Quality Gate; `01-questions.md` — **сигнальный** артефакт, **не** machine-verdict). (1) **Контракт + канон.** `.openclaw/agents/analyst.md` документирует канал «блокирующие вопросы → `01-questions.md`, НЕ фабриковать deliverables» + поведение на resume; новый скелет `docs/_templates/01-questions.md`; строка манифеста + примечание о префиксе `01-` в `docs/_standards/PIPELINE_DOCS.md`. (2) **Приоритет «вопросы активны» > «файлы готовы»** в `_handle_analysis_approved_flow` (DQ-3): чистая логика решения вынесена в leaf `src/analyst_questions.py` (`questions_gate_applies`/`autopause_applies`/`questions_active`), side-effects — в `stage_engine` (`_decide_analysis_outcome`/`_emit_analysis_needs_input`/`_emit_analysis_in_review`/`_emit_analysis_empty`); блокирующие вопросы достигают Needs Input даже при сфабрикованном полном пакете. (3) **Авто-park (DQ-1)** при Needs Input через ось «пауза» ORCH-124 (`db.set_task_paused`) → задача исключается из «активного» предиката serial-gate (ORCH-088), FIFO репо не клинит, пока ждём человека; **resume + unpark** в `handle_status_start` (analysis-ветка, `db.clear_task_paused`). (4) **Гигиена устаревания (DQ-2)** — детерминированный offline freshness-supersede по `mtime` (вопросы активны, пока пакет неполон ИЛИ `01-questions.md` не старше всех 4 deliverables) → полный свежий пакет supersede’ит старый файл без зависимости от LLM (нет бесконечной петли Needs Input). Флаги (`config.py`, безопасные дефолты): `analyst_questions_gate_enabled` (kill-switch) / `analyst_questions_gate_repos` (CSV; **пусто → self-hosting only**) / `analyst_needs_input_autopause_enabled` (независимый тумблер авто-park/unpark; `False` → operator-park `POST /serial-gate/pause`). off/out-of-scope → байт-в-байт как до ORCH-120 (enduro не затронут); ORCH-066 (Needs Input только у аналитика) не расширяется. Покрытие — `tests/test_orch120_analyst_needs_input.py` (TC-01 обязательный регресс: красный до фикса, зелёный после), `tests/test_orch120_serial_gate_needs_input.py`, `tests/test_orch120_resume_unpark.py`, `tests/test_orch120_questions_artifact_canon.py` + assert в `tests/test_agent_prompts_canon.py`. Витрина системы `docs/overview/` обновлена в том же PR (ось ORCH-011): абзац пауз `tech-pipeline.md` и пункт `GET /queue` в `tech-observability.md` теперь называют **два** источника паузы (оператор + авто-park движком на Needs Input), `tech-agents.md` — when-applicable сигнальный канал `01-questions.md` у `analyst` (`tests/test_system_docs.py` зелёный). ADR: `docs/work-items/ORCH-120/06-adr/ADR-001-analyst-open-questions-needs-input.md`, сквозной `docs/architecture/adr/adr-0053-analyst-open-questions-needs-input-flow.md`.
|
||||
- **Гигиена run-ownership строки `jobs` — инвариант «queued ⇒ run_id/pid/started_at IS NULL»** (ORCH-126, `fix`, трек Bug): багфикс контрол-плейна (инцидент ORCH-124/125) — при `ORCH_SERIAL_GATE_ENABLED=false` queued analyst-job'ы зависали навсегда (job 2286: `status=queued + run_id=759/760 + pid=35/42 + started_at=NULL` — физически невозможное состояние). **Причина:** ни один путь возврата job в `queued` (restart `requeue_running_jobs` / retry `mark_job('queued')` / transient `mark_job_transient` / reap `reap_running_job('queued')`) **не сбрасывал run-ownership** (`run_id`/`pid`); после рестарта контейнера pid мог быть **переиспользован** ОС → `pid_alive(stale)=True` → job-reaper (ORCH-065) Tier-1 «видел живой» фантомный `running` и при `max_concurrency=1` клинил клейм **всей** общей очереди всех проектов. **Инвариант (adr-0052):** `status='queued' ⇒ run_id IS NULL AND pid IS NULL AND started_at IS NULL` — queued-job никогда не несёт run-ownership (история run'а — в `agent_runs`, не в `jobs.run_id`). Фикс на **существующих колонках**: `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / `check_*` / machine-verdict-ключи / **схема БД** — байт-в-байт не тронуты; для здоровых job'ов и enduro поведение байт-в-байт; миграция не требуется. ADR: `docs/work-items/ORCH-126/06-adr/ADR-001-queued-job-run-ownership-hygiene.md`, сквозной `docs/architecture/adr/adr-0052-queued-job-run-ownership-invariant.md`.
|
||||
- **D1 — Forward-cleanup на всех путях возврата в `queued` (FR-1/AC-1):** `requeue_running_jobs` / `mark_job('queued')` / `mark_job_transient` / `reap_running_job('queued')` выставляют `run_id=NULL, pid=NULL` той же UPDATE-транзакцией, что чистит `started_at`/`finished_at`. Атомарные `status`-guard'ы (`reap_running_job … WHERE status='running'`, rowcount) — **сохранены байт-в-байт** (restart-safe, гонка worker↔reaper↔monitor — TR-4). Каллер-переданный `run_id` для `queued` **игнорируется** (инвариант важнее: `launcher._finalize_permanent`/reaper по-прежнему передают старый `run_id`, но для `queued` он сбрасывается). Безусловно — исправление инварианта данных, без флага (D6).
|
||||
- **D2 — Чистый claim (FR-2/AC-3):** `claim_next_job` при флипе `queued→running` сбрасывает `pid=NULL, run_id=NULL` тем же существующим UPDATE (defense-in-depth поверх D1) → между claim и стампом `pid` в `_spawn` строка несёт `pid IS NULL`, не чужой pid. SELECT-гейт (`status='queued' AND available_at<=now` + dep/serial-gate) — **не тронут** (offline hot-path, NFR-2; без нового SELECT/сети).
|
||||
|
||||
@@ -47,6 +47,7 @@ check_tests_passed → check_staging_status → check_deploy_status`.
|
||||
|----------|----------------|-----------|------------------|--------------------------|-------------------------|
|
||||
| `00-business-request.md` | система (Plane webhook `_create_initial_docs`) / заказчик | required | `created` (инициализация) | не гейтится (вход) | — |
|
||||
| `01-brd.md` | analyst | required | `analysis` | exit-гейт `analysis→architecture` = `check_analysis_approved` (Approved + полнота файлов); helper `check_analysis_complete` (наличие `01/02/03/04`) | — |
|
||||
| `01-questions.md` | analyst | when-applicable | `analysis` | **сигнальный** (гейтом НЕ парсится); механизм — ветка Needs Input в `_handle_analysis_approved_flow` (ORCH-120, adr-0053): активные блокирующие вопросы → `set_issue_needs_input` (приоритет над «файлы готовы») | — (не machine-verdict) |
|
||||
| `02-trz.md` | analyst | required | `analysis` | то же | — |
|
||||
| `03-acceptance-criteria.md` | analyst | required | `analysis` | то же | — |
|
||||
| `04-test-plan.yaml` | analyst | required | `analysis` | то же | — |
|
||||
@@ -72,6 +73,10 @@ check_tests_passed → check_staging_status → check_deploy_status`.
|
||||
- **Категория `when-applicable`** = документ пишется при наличии соответствующего предмета
|
||||
(инфра / данные / security / post-deploy). Его отсутствие — не нарушение приёмки.
|
||||
- **`05-…` / `09-…` / `11-…`** — зарезервированные/legacy номера, в текущем каноне не используются.
|
||||
- **Префикс `01-` (DQ-4 ORCH-120)** — общий для артефактов стадии `analysis` владельца `analyst`:
|
||||
`01-brd.md` — обязательный deliverable (гейтится `check_analysis_complete`), `01-questions.md` —
|
||||
**сигнальный** when-applicable артефакт того же владельца/стадии. Коллизии нет: файлы разноимённые,
|
||||
`check_analysis_complete` проверяет ровно `01-brd.md`/`02`/`03`/`04` (`01-questions.md` им не парсится).
|
||||
|
||||
---
|
||||
|
||||
|
||||
43
docs/_templates/01-questions.md
vendored
Normal file
43
docs/_templates/01-questions.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
work_item: ORCH-NNN
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: needs-input
|
||||
created_at: <YYYY-MM-DD>
|
||||
model_used: <resolve ORCH-41>
|
||||
---
|
||||
|
||||
# 01 — Открытые вопросы (Open Questions): ORCH-NNN — <название>
|
||||
|
||||
Work Item: **ORCH-NNN** · Repo: **<repo>** · Стадия: analysis
|
||||
|
||||
> **Сигнальный** when-applicable артефакт (ORCH-120, adr-0053). Пишется аналитиком через **Write
|
||||
> tool** ТОЛЬКО при **блокирующей** неоднозначности бизнес-запроса, когда выпустить корректные 4
|
||||
> deliverables нельзя без ответа заказчика. Наличие этого файла с **активными** вопросами уводит
|
||||
> задачу в **Needs Input** (приоритет над «файлы готовы»). **Не** machine-verdict: гейтом
|
||||
> (`check_analysis_complete`/`check_analysis_approved`) НЕ парсится — это сигнал движку
|
||||
> (`_handle_analysis_approved_flow`).
|
||||
>
|
||||
> ⚠️ Если блокирующих вопросов НЕТ — **не создавай** этот файл; выпускай полный пакет (`01-brd.md`/
|
||||
> `02-trz.md`/`03-acceptance-criteria.md`/`04-test-plan.yaml`). Не фабрикуй требования ради сдачи 4
|
||||
> файлов.
|
||||
|
||||
## 1. Контекст
|
||||
<Что именно в бизнес-запросе (`00-business-request.md`) блокирует выпуск корректного пакета. Какие
|
||||
факты установлены, а какие — нет. На какой код `src/` это влияет.>
|
||||
|
||||
## 2. Блокирующие вопросы
|
||||
> Каждый вопрос — конкретный, отвечаемый, с вариантами (где уместно) и указанием, почему ответ
|
||||
> блокирует анализ. Нумеруй (Q-1, Q-2, …).
|
||||
|
||||
- **Q-1** — <вопрос>
|
||||
- Вариант A: <…> (последствие)
|
||||
- Вариант B: <…> (последствие)
|
||||
- Почему блокирует: <без ответа нельзя выпустить BR/TRZ, т.к. …>
|
||||
- **Q-2** — …
|
||||
|
||||
## 3. Что разблокирует анализ
|
||||
<Какие ответы переводят задачу из Needs Input обратно в работу: после ответов заказчика в Plane
|
||||
аналитик перезапускается (resume), читает свежие комментарии и выпускает полный пакет. Если часть
|
||||
вопросов снята, а часть осталась — **перепиши** этот файл (оставь только актуальные блокеры), иначе
|
||||
выпусти 4 deliverables (свежий пакет supersede’ит этот файл по mtime, DQ-2).>
|
||||
@@ -650,6 +650,35 @@ ORCH-027 вводит детерминированный (без LLM) **гейт
|
||||
`docs/work-items/ORCH-088/08-data-requirements.md`,
|
||||
`docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md`.
|
||||
|
||||
### Открытые вопросы аналитика → Needs Input (ORCH-120 — реализовано, [adr-0053](adr/adr-0053-analyst-open-questions-needs-input-flow.md))
|
||||
При неоднозначном бизнес-запросе у аналитика не было рабочего канала уточнения — он **фабриковал**
|
||||
требования, чтобы сдать обязательные 4 файла. Механизм «вопросы → Needs Input» в
|
||||
`_handle_analysis_approved_flow` (`src/stage_engine.py`) существовал, но был **мёртв** (контракт не в
|
||||
промпте; ветка `files_ok` имела приоритет; Needs Input клинил serial-gate; нет гигиены устаревшего
|
||||
`01-questions.md`). ORCH-120 **активирует и достраивает** путь — аддитивно, под kill-switch, скоуп
|
||||
self-hosting, never-raise; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД —
|
||||
**байт-в-байт не тронуты** (поток — pre-gate-ветка движка, **не** QG; `01-questions.md` — сигнальный
|
||||
артефакт, **не** machine-verdict).
|
||||
- **Контракт + канон.** `.openclaw/agents/analyst.md` документирует «блокирующие вопросы →
|
||||
`01-questions.md`, НЕ фабриковать deliverables»; `01-questions.md` стандартизирован как
|
||||
`when-applicable` сигнальный артефакт (скелет `docs/_templates/` + строка `PIPELINE_DOCS.md`).
|
||||
- **Приоритет «вопросы активны» > «файлы готовы»** в `_handle_analysis_approved_flow` (DQ-3) →
|
||||
блокирующие вопросы достигают Needs Input даже при частичных/сфабрикованных deliverables. Чистая
|
||||
логика решения — leaf `src/analyst_questions.py` (`questions_gate_applies`/`autopause_applies`/
|
||||
`questions_active`, never-raise); side-effects (`set_issue_needs_input`/коммент/Telegram/park) —
|
||||
в `stage_engine` (`_decide_analysis_outcome`/`_emit_analysis_*`).
|
||||
- **Авто-park (DQ-1)** через ось «пауза» ORCH-124 (`db.set_task_paused` при Needs Input) → задача
|
||||
исключается из «активного» предиката serial-gate, FIFO репо не клинит, пока ждём человека;
|
||||
**resume + unpark** в `handle_status_start` (analysis-ветка, `clear_task_paused`).
|
||||
- **Устаревание (DQ-2)** — детерминированный offline freshness-supersede по mtime (вопросы активны,
|
||||
пока пакет неполон ИЛИ `01-questions.md` не старше всех 4 deliverables) → полный свежий пакет
|
||||
supersede’ит старый файл без зависимости от LLM.
|
||||
- **Флаги** (`config.py`): `analyst_questions_gate_enabled` (kill-switch) / `analyst_questions_gate_repos`
|
||||
(CSV; **пусто → self-hosting only**) / `analyst_needs_input_autopause_enabled` (независимый тумблер
|
||||
авто-park/unpark; `False` → operator-park `POST /serial-gate/pause`). off/out-of-scope → байт-в-байт
|
||||
как до ORCH-120 (enduro не затронут). ORCH-066 (Needs Input только у аналитика) не расширяется.
|
||||
Детали — `docs/work-items/ORCH-120/06-adr/ADR-001-analyst-open-questions-needs-input.md`.
|
||||
|
||||
### Авто-режим по лейблам: autoApprove + autoDeploy (ORCH-089 — реализовано)
|
||||
Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон (эпик
|
||||
ORCH-088): гейт BRD (`analysis`: ждёт ручного `Approved`) и гейт прод-деплоя (`deploy`:
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
work_item: ORCH-120
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# ADR-0053: Поток «открытые вопросы аналитика → Needs Input» (приоритет + пауза + resume)
|
||||
|
||||
Сквозной (cross-cutting) ADR. Детальное решение задачи —
|
||||
`docs/work-items/ORCH-120/06-adr/ADR-001-analyst-open-questions-needs-input.md`.
|
||||
|
||||
Статус: **Proposed** · Дата: 2026-06-17 · Источник: **ORCH-120** (bug → escalate full-cycle)
|
||||
|
||||
## Контекст
|
||||
|
||||
Конвейер обязывает аналитика выпустить 4 файла (`01-brd`/`02-trz`/`03-acceptance-criteria`/
|
||||
`04-test-plan.yaml`), иначе exit-гейт `analysis` не пройдёт. При неоднозначном бизнес-запросе
|
||||
(классика — `Description: TBD`) у аналитика нет рабочего канала уточнения → он **фабрикует**
|
||||
требования. Механизм «вопросы → Needs Input» в `_handle_analysis_approved_flow`
|
||||
(`src/stage_engine.py`) **существует, но мёртв** из-за четырёх смежных дефектов: контракт не
|
||||
доведён до промпта; ветка `files_ok` имеет приоритет над веткой вопросов; Needs Input клинит
|
||||
serial-gate репо (ORCH-088); нет гигиены устаревшего `01-questions.md`.
|
||||
|
||||
Поток пересекает несколько подсистем, поэтому фиксируется сквозным ADR (анти-археология ORCH-078:
|
||||
блок `_handle_analysis_approved_flow` несёт 3+ маркера — ORCH-066/088/089/124):
|
||||
- **ORCH-066** — Needs Input принадлежит **только** аналитику (слой B индикации ≠ слой A стадий).
|
||||
- **ORCH-088** — per-repo serial-gate: «активная задача» по `tasks.stage NOT IN ('done','cancelled')`.
|
||||
- **ORCH-124** (adr-0051) — ортогональная ось «пауза» (`tasks.paused_at`): исключает задачу из
|
||||
«активного» предиката, **не** обходя оси `task_deps`/`repo_freeze`/терминал.
|
||||
- **ORCH-089** — autoApprove (человеческий BRD-гейт по лейблу) в той же ветке `files_ok`.
|
||||
|
||||
## Решение
|
||||
|
||||
**Активировать мёртвый путь четырьмя согласованными изменениями** — аддитивно, под kill-switch,
|
||||
скоуп self-hosting, never-raise:
|
||||
|
||||
1. **Контракт промпта + канон артефакта.** `.openclaw/agents/analyst.md` документирует канал
|
||||
«блокирующие вопросы → `01-questions.md`, НЕ фабриковать deliverables»; `01-questions.md`
|
||||
стандартизирован как **сигнальный** when-applicable артефакт (скелет `docs/_templates/` +
|
||||
строка манифеста `PIPELINE_DOCS.md`) — **не** machine-verdict (гейтом не парсится, BR-6).
|
||||
2. **Приоритет «вопросы активны» > «файлы готовы».** В `_handle_analysis_approved_flow` предикат
|
||||
активных вопросов проверяется **до** ветки `files_ok` → блокирующие вопросы надёжно достигают
|
||||
Needs Input даже при частичных/сфабрикованных deliverables.
|
||||
3. **Авто-park через ось «пауза» ORCH-124.** Переход в Needs Input вызывает `db.set_task_paused`
|
||||
→ задача исключается из «активного» предиката serial-gate → следующая задача репо входит в
|
||||
`analysis`, пока первая ждёт человека (не клинит FIFO неопределённо долго).
|
||||
4. **Resume + unpark.** `handle_status_start` (analysis-resume) снимает паузу (`clear_task_paused`)
|
||||
и перезапускает аналитика; relaunch-guard ORCH-090 (только `analysis`) не ослаблен.
|
||||
|
||||
**Устаревание `01-questions.md` (детерминированно, offline):** freshness-gated supersede по mtime —
|
||||
вопросы «активны», пока пакет неполон ИЛИ `01-questions.md` не старше всех 4 deliverables; полный
|
||||
свежий пакет supersede’ит старый файл (выбор механизма и отвергнутые альтернативы — ADR-001 DQ-2).
|
||||
|
||||
## Инварианты (нормативно)
|
||||
|
||||
- **Поток — pre-gate-ветка движка, НЕ Quality Gate.** `STAGE_TRANSITIONS` / реестр и имена
|
||||
`QG_CHECKS` / семантика `check_analysis_complete`/`check_analysis_approved` / machine-verdict-ключи
|
||||
/ схемы существующих таблиц — **байт-в-байт не тронуты**.
|
||||
- **Без схемы БД:** переиспользуется `tasks.paused_at` (ORCH-124); новых таблиц/колонок нет.
|
||||
- **ORCH-066 не расширяется:** Needs Input остаётся **только** у аналитика.
|
||||
- **ORCH-124 не регрессирует:** пауза ортогональна — оси `task_deps`/`repo_freeze`/терминал
|
||||
`{done,cancelled}` `paused_at` **не читают**; анти-stale-base ORCH-088 цел (нормальная задача
|
||||
`paused_at IS NULL` держит гейт; свежесть базы на resume — существующими механизмами).
|
||||
- **Self-hosting-безопасность:** поток только меняет Plane-статус/паузу/коммент и читает worktree —
|
||||
не деплоит, не рестартит прод-контейнер, не пушит в `main`, не трогает detached-процессы.
|
||||
- **never-raise / обратимость:** все врезки изолированы и деградируют к прежнему поведению;
|
||||
3 флага (`analyst_questions_gate_enabled` / `analyst_questions_gate_repos` /
|
||||
`analyst_needs_input_autopause_enabled`) с безопасными дефолтами → off/out-of-scope = байт-в-байт
|
||||
как до ORCH-120 (enduro не затронут).
|
||||
|
||||
## Последствия
|
||||
|
||||
Конвейер перестаёт строить решения поверх домыслов; serial-gate не клинит на задаче, ждущей
|
||||
человека (поддержка автономного пакетного прогона ORCH-088); аналитик получает легитимный канал
|
||||
уточнения. Цена — узкое связывание индикации с осью планировщика при авто-park (смягчено флагом +
|
||||
узким триггером + never-raise) и зависимость supersede от mtime (смягчено: полный прогон всегда
|
||||
пишет свежие deliverables + контракт промпта). Детали, альтернативы и риски —
|
||||
`docs/work-items/ORCH-120/06-adr/ADR-001-analyst-open-questions-needs-input.md`,
|
||||
`docs/work-items/ORCH-120/10-tech-risks.md`.
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
| Роль | Стадия | Вход | Выходные артефакты | Machine-verdict ключ |
|
||||
|------|--------|------|--------------------|----------------------|
|
||||
| `analyst` | analysis | бизнес-запрос (`00-business-request.md`) | `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml` | — (гейт проверяет полноту пакета + одобрение человека) |
|
||||
| `analyst` | analysis | бизнес-запрос (`00-business-request.md`) | `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`; when-applicable сигнальный `01-questions.md` | — (гейт проверяет полноту пакета + одобрение человека) |
|
||||
| `architect` | architecture | пакет аналитики | `06-adr/ADR-NNN-*.md`, when-applicable `07-infra-requirements.md` / `08-data-requirements.md`, `10-tech-risks.md` | — (гейт проверяет наличие ADR) |
|
||||
| `developer` | development | ТЗ + ADR | код в `src/`, тесты в `tests/`, обновлённые доки, `CHANGELOG.md`, PR в Gitea | — (гейт — зелёный CI ветки) |
|
||||
| `reviewer` | review | PR diff + ТЗ/ADR | `12-review.md` | `verdict:` (`APPROVED` \| `REQUEST_CHANGES`) |
|
||||
@@ -18,6 +18,15 @@
|
||||
Machine-verdict ключи читаются гейтами **только из YAML-frontmatter** артефакта (никогда из
|
||||
прозы) и неизменны байт-в-байт — подробнее в [блоке качества](tech-quality-security.md).
|
||||
|
||||
> **Сигнальный канал аналитика → Needs Input (ORCH-120).** Если на стадии `analysis` аналитик
|
||||
> упирается в блокирующие открытые вопросы, он не фабрикует обязательные deliverables, а выпускает
|
||||
> when-applicable артефакт `01-questions.md` — задача уходит в **Needs Input** и (под флагом
|
||||
> `analyst_needs_input_autopause_enabled`, скоуп self-hosting) автоматически встаёт на паузу, чтобы
|
||||
> не клинить очередь репозитория, пока ждём ответа человека; ответ возобновляет анализ и снимает
|
||||
> паузу. `01-questions.md` — сигнальный артефакт того же владельца/стадии, **не** machine-verdict и
|
||||
> **не** один из 4 обязательных deliverables (exit-гейт `check_analysis_complete` его не парсит). Как
|
||||
> это вплетено в serial-gate — [конвейер](tech-pipeline.md).
|
||||
|
||||
## Модель и эффорт
|
||||
|
||||
Модель и эффорт каждой роли резолвятся **только из конфига** (не из промпта); текущие
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
- **`GET /queue`** — человекочитаемый снимок всего конвейера: очередь и job'ы, состояние
|
||||
serial gate (заморозки, паузы задач, причина ожидания успешника), авто-лейблы, багфикс-трек,
|
||||
coverage, журнал уроков, владение переходами (`transition_lease`), фоновые демоны. Первая
|
||||
точка диагностики «что сейчас происходит». Паузу/возобновление задачи в serial gate оператор
|
||||
включает явными эндпоинтами `POST /serial-gate/pause|resume`.
|
||||
точка диагностики «что сейчас происходит». Паузу/возобновление задачи в serial gate включают
|
||||
два источника: **оператор** — явными эндпоинтами `POST /serial-gate/pause|resume`, и **движок** —
|
||||
автоматически, когда аналитик задаёт блокирующие вопросы и задача уходит в Needs Input (авто-park;
|
||||
снимается на возобновлении; под флагом `analyst_needs_input_autopause_enabled`, скоуп self-hosting).
|
||||
- **`GET /metrics`** — машинный контракт для внешнего наблюдателя (версионированная схема):
|
||||
health, возраст последних событий, счётчики сбоев.
|
||||
- **`GET /health`** — живость процесса.
|
||||
|
||||
@@ -107,12 +107,17 @@ created → analysis → architecture → development → review → testing →
|
||||
прода после выкладки замораживает репозиторий (freeze) до ручного разбора — следующие задачи
|
||||
ждут.
|
||||
|
||||
У FIFO-порядка есть управляемое исключение — **пауза без блокировки**: оператор может явно
|
||||
поставить более раннюю задачу на паузу (durable-сигнал `tasks.paused_at`), и тогда срочный
|
||||
успешник её обгоняет, не дожидаясь завершения. Пауза — отдельная ось: она ≠ отмена (задача не
|
||||
терминальна и возвращается в гейт обратной командой) и **не** обходит ни freeze, ни объявленные
|
||||
зависимости. Свежесть базы возобновлённой задачи гарантируют те же механизмы (отложенный срез
|
||||
ветки + ребейз на слиянии), что и для обычного FIFO.
|
||||
У FIFO-порядка есть управляемое исключение — **пауза без блокировки**: более раннюю задачу можно
|
||||
снять с активной очереди репозитория, не дожидаясь её завершения, и тогда срочный успешник её
|
||||
обгоняет. Паузу (durable-сигнал `tasks.paused_at`) ставят два источника. **Оператор** — явно
|
||||
(`POST /serial-gate/pause`, снять — `/resume`). **Движок** — автоматически, когда аналитик
|
||||
упирается в блокирующие открытые вопросы и задача уходит в **Needs Input** (узкий триггер под
|
||||
флагом `analyst_needs_input_autopause_enabled`, скоуп self-hosting); на возобновлении (ответ
|
||||
человека) движок снимает паузу симметрично. Авто-park нужен, чтобы задача, ждущая человека часы
|
||||
или дни, не клинила FIFO-очередь репозитория в автономном пакетном прогоне. Пауза — отдельная ось:
|
||||
она ≠ отмена (задача не терминальна и возвращается в гейт обратной командой) и **не** обходит ни
|
||||
freeze, ни объявленные зависимости. Свежесть базы возобновлённой задачи гарантируют те же
|
||||
механизмы (отложенный срез ветки + ребейз на слиянии), что и для обычного FIFO.
|
||||
|
||||
## Отмена: STOP → `cancelled`
|
||||
|
||||
|
||||
7
docs/work-items/ORCH-120/00-business-request.md
Normal file
7
docs/work-items/ORCH-120/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: BUG: analyst open questions must move task to Needs Input
|
||||
|
||||
Work Item ID: ORCH-120
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
171
docs/work-items/ORCH-120/01-brd.md
Normal file
171
docs/work-items/ORCH-120/01-brd.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
work_item: ORCH-120
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
escalate: full-cycle
|
||||
---
|
||||
|
||||
# 01 — BRD (бизнес-требования): ORCH-120 — Открытые вопросы аналитика должны переводить задачу в Needs Input
|
||||
|
||||
Work Item: **ORCH-120** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
> ⚠️ **Эскалация в полный цикл (`escalate: full-cycle`).** Это формально баг (метка `BUG:` в
|
||||
> заголовке), но фикс требует архитектурных решений (правило приоритета веток в
|
||||
> `_handle_analysis_approved_flow`, интеграция с осью «пауза» ORCH-124, семантика устаревания
|
||||
> `01-questions.md`, стандартизация нового pipeline-артефакта) — нужен ADR. Поэтому выпущен
|
||||
> **полный** analysis-пакет (01/02/03/04), а не облегчённый bug-shaped. Оператор снимает багфикс-трек
|
||||
> командой `POST /bug-fast-track/escalate?work_item=ORCH-120`, после чего задача идёт через стадию
|
||||
> `architecture` (ADR-001 D5 ORCH-019). Открытые проектные вопросы для архитектора — §6 (DQ-1…DQ-4).
|
||||
|
||||
## 1. Бизнес-контекст и проблема
|
||||
|
||||
При запуске конвейера аналитик (`analyst`) получает бизнес-запрос и **обязан** выпустить 4 файла
|
||||
(`01-brd` / `02-trz` / `03-acceptance-criteria` / `04-test-plan.yaml`), иначе exit-гейт стадии
|
||||
`analysis` не пройдёт. Если бизнес-запрос неоднозначен или неполон (классический пример — тело
|
||||
запроса `Description: TBD`), у аналитика **нет рабочего канала** запросить уточнения у заказчика: он
|
||||
вынужден **домысливать** требования и всё равно сдать 4 файла. Сфабрикованный пакет уходит в
|
||||
`In Review` / `architecture` — то есть весь конвейер строит решение поверх выдуманных требований.
|
||||
|
||||
**Парадокс:** механизм «вопросы → Needs Input» в движке **уже есть, но мёртв**. Код
|
||||
`src/stage_engine.py::_handle_analysis_approved_flow` (стр. 769–786) читает файл
|
||||
`docs/work-items/<wid>/01-questions.md` и при его наличии вызывает `set_issue_needs_input(...)` +
|
||||
комментарий в Plane + Telegram. Однако:
|
||||
|
||||
1. **Контракт не доведён до аналитика.** Промпт `.openclaw/agents/analyst.md` **нигде** не упоминает
|
||||
`01-questions.md`: ни в `<deliverables>`, ни в `<task>`. Скелета `docs/_templates/01-questions.md`
|
||||
нет, в манифесте `docs/_standards/PIPELINE_DOCS.md` артефакт не описан. Аналитик физически не
|
||||
знает, что у него есть канал «задать блокирующий вопрос», поэтому домысливает.
|
||||
2. **Ошибка приоритета веток.** В `_handle_analysis_approved_flow` ветка `files_ok` (все 4 файла на
|
||||
месте — `check_analysis_complete`) проверяется **первой** и делает `return` (стр. 711–767). Ветка
|
||||
`01-questions.md` (стр. 769) достижима, только если 4 файла НЕ полны. Значит, если аналитик сдал и
|
||||
неполный/заглушечный пакет, и `01-questions.md` — движок уйдёт в `In Review`, проигнорировав
|
||||
блокирующие вопросы. «Есть вопросы» должно иметь приоритет над «файлы на месте».
|
||||
3. **Needs-Input блокирует serial-gate репо.** Задача в Needs Input остаётся в стадии `analysis`
|
||||
(Plane-статус — слой B индикации, ORCH-066, **не** меняет `tasks.stage`) и при этом
|
||||
`paused_at IS NULL`. По правилу serial-gate (ORCH-088) такая «активная» задача держит FIFO репо
|
||||
закрытым: пока заказчик не ответит (часы/дни), ни одна следующая задача `orchestrator` не войдёт в
|
||||
`analysis`. ORCH-124 ввёл ортогональную ось «пауза» (`tasks.paused_at` + `POST /serial-gate/pause|
|
||||
resume`) ровно для случая «приостановлено, но не блокирует» — Needs-Input обязан её использовать.
|
||||
4. **Нет гигиены устаревшего `01-questions.md`.** После ответа заказчика `handle_status_start`
|
||||
перезапускает аналитика (`src/webhooks/plane.py:317–381`). Если перезапущенный аналитик теперь
|
||||
выпускает полный валидный пакет, старый `01-questions.md` остаётся в ветке. Без правила
|
||||
«устаревания» он либо игнорируется (если `files_ok` побеждает), либо вечно перезапускает Needs
|
||||
Input (если вопросы получат приоритет). Нужно явное правило supersede.
|
||||
|
||||
Корень — **разрыв контракта между промптом аналитика и движком** плюс **3 смежных дефекта потока**
|
||||
(приоритет, блокировка очереди, устаревание). ORCH-120 закрывает их как единый «правильный поток
|
||||
Needs Input».
|
||||
|
||||
**Связь с предшественниками (контекст резюма из бэклога):** задача разморожена после корневых
|
||||
фиксов **ORCH-124** (ось «пауза без блокировки» — необходимый фундамент для требования BR-3) и
|
||||
**ORCH-126** (queued-job не застревает со stale `run_id`/`pid` — гарантирует, что перезапущенный
|
||||
после ответа аналитик-job реально заберётся из очереди). Оба — предусловия, а не объём ORCH-120.
|
||||
|
||||
## 2. Объём (scope)
|
||||
|
||||
### В объёме
|
||||
- **Контракт промпта аналитика:** `.openclaw/agents/analyst.md` явно документирует канал
|
||||
«блокирующие открытые вопросы → пиши `01-questions.md`, НЕ фабрикуй 4 deliverables», с форматом и
|
||||
правилом поведения на перезапуске (прочитать ответы, снять устаревшие вопросы).
|
||||
- **Канон артефакта:** скелет `docs/_templates/01-questions.md` + строка в манифесте
|
||||
`docs/_standards/PIPELINE_DOCS.md` (артефакт-сигнал Needs Input; **не** machine-verdict-док, гейтом
|
||||
не парсится).
|
||||
- **Приоритет веток в движке:** в `_handle_analysis_approved_flow` блокирующие открытые вопросы
|
||||
получают корректный приоритет → задача с вопросами надёжно достигает Needs Input.
|
||||
- **Неблокирование serial-gate:** переход в Needs Input не держит FIFO репо закрытым неопределённо
|
||||
долго (интеграция с осью «пауза» ORCH-124).
|
||||
- **Гигиена устаревания:** перезапущенный аналитик, выпустивший полный валидный пакет без свежих
|
||||
вопросов, приводит к `In Review`, а не к повторному Needs Input.
|
||||
- **Корректность resume-петли:** ответ заказчика → перезапуск аналитика → снятие паузы (unpark), job
|
||||
забирается из очереди.
|
||||
- **Обязательный регресс-тест** (красный до фикса, зелёный после) + анти-дрейф структурные тесты.
|
||||
|
||||
### Вне объёма
|
||||
- **Расширение владения Needs Input на других агентов.** ORCH-066 BR-10 фиксирует: Needs Input —
|
||||
только у аналитика. Механизм не расширяется на architect/developer/reviewer/tester/deployer.
|
||||
- **Новые QG-проверки и новые рёбра `STAGE_TRANSITIONS`.** Поток вопросов — pre-gate-ветка движка,
|
||||
не Quality Gate. `check_analysis_complete`/`check_analysis_approved` — байт-в-байт.
|
||||
- **Изменение семантики самого гейта `analysis`** (4 файла по-прежнему обязательны для прохождения
|
||||
exit-гейта `analysis → architecture`).
|
||||
- **Авто-ответ на вопросы / LLM-триаж ответов заказчика.** Ответы читает человек/аналитик, а не
|
||||
отдельный автомат.
|
||||
- **Машинерия багфикс-трека (ORCH-019)** и любые изменения вне аналитической стадии.
|
||||
|
||||
## 3. Заинтересованные стороны
|
||||
- **Заказчик / оператор (Слава)** — получает осмысленный запрос уточнений вместо выдуманных
|
||||
требований; отвечает в Plane и возвращает задачу в работу.
|
||||
- **Конвейер `orchestrator` (self-hosting)** — перестаёт строить решения поверх домыслов; serial-gate
|
||||
репо не клинит на задаче, ждущей человека.
|
||||
- **Аналитик-агент** — получает легитимный канал «не знаю — спрошу» вместо принуждения к фабрикации.
|
||||
- **Другие проекты на общем инстансе (enduro-trails)** — не затронуты (нулевая регрессия при
|
||||
отсутствии `01-questions.md` и вне self-hosting-области).
|
||||
|
||||
## 4. Бизнес-требования (BR)
|
||||
- **BR-1** — Аналитик, столкнувшийся с **блокирующей** неоднозначностью бизнес-запроса, ОБЯЗАН иметь
|
||||
документированный канал запроса уточнений (`01-questions.md`) и НЕ должен фабриковать 4 deliverables
|
||||
«лишь бы пройти гейт». Промпт `.openclaw/agents/analyst.md` описывает этот канал.
|
||||
- **BR-2** — Наличие блокирующих открытых вопросов переводит задачу в Plane-статус **Needs Input** и
|
||||
**останавливает** продвижение по конвейеру (не `In Review`, не `architecture`), даже если на диске
|
||||
присутствуют частичные/заглушечные deliverables. Приоритет «вопросы» > «файлы на месте».
|
||||
- **BR-3** — Задача в Needs Input **не блокирует** per-repo serial-gate FIFO неопределённо долго:
|
||||
следующая задача `orchestrator` может войти в `analysis`, пока первая ждёт ответа человека.
|
||||
- **BR-4** — После ответа заказчика (возврат issue в рабочий статус) аналитик перезапускается, читает
|
||||
ответы и выпускает пакет. Если пакет полон и валиден и свежих блокирующих вопросов нет → задача
|
||||
переходит в `In Review` (устаревший `01-questions.md` не должен повторно ронять её в Needs Input).
|
||||
- **BR-5** — Поведение **обратимо и выборочно**: при отсутствии `01-questions.md` и выключенных
|
||||
под-флагах поток Needs Input/паузы — байт-в-байт как до ORCH-120 (нулевая регрессия для enduro и
|
||||
для штатной задачи без вопросов).
|
||||
- **BR-6** — `01-questions.md` стандартизирован как pipeline-артефакт (скелет в `docs/_templates/` +
|
||||
строка манифеста `PIPELINE_DOCS.md`): он сигнальный, **не** machine-verdict (гейтом не парсится).
|
||||
|
||||
## 5. Нефункциональные требования (NFR)
|
||||
- **NFR-1 (never-raise / fail-safe)** — Любая ошибка новой логики (чтение файла, park-вызов,
|
||||
определение приоритета) НЕ роняет `advance_stage`/launcher и деградирует к безопасному прежнему
|
||||
поведению (как существующие leaf'ы `serial_gate`/`labels`/`cancel`).
|
||||
- **NFR-2 (обратная совместимость)** — Стадии, кроме `analysis`, и Needs-Input-владение (ORCH-066) —
|
||||
не трогаются. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи / семантика
|
||||
exit-гейта `analysis` — байт-в-байт.
|
||||
- **NFR-3 (инварианты serial-gate)** — Интеграция с паузой не регрессирует ORCH-088 (анти-stale-base:
|
||||
отложенный срез ветки) и ORCH-124 (терминал `{done,cancelled}` и оси `task_deps`/`freeze` —
|
||||
не читают `paused_at`; пауза их не обходит).
|
||||
- **NFR-4 (self-hosting-безопасность)** — Поток только меняет Plane-статус/паузу/комментарий и читает
|
||||
worktree: не деплоит, не рестартит прод-контейнер, не пушит в `main`, не трогает detached-процессы.
|
||||
- **NFR-5 (наблюдаемость)** — Переход в Needs Input и park/unpark логируются структурно; состояние
|
||||
паузы видно в блоке `serial_gate` `GET /queue` (ORCH-124 уже отдаёт `paused`).
|
||||
|
||||
## 6. Допущения и ограничения
|
||||
- **Допущение:** механизм чтения `01-questions.md` и `set_issue_needs_input` рабочие — задача в
|
||||
основном **активирует и достраивает** существующий путь, а не строит его с нуля.
|
||||
- **Допущение:** промпт `cat`-ается из worktree в момент запуска (ORCH-077 loading-model) → новый
|
||||
контракт аналитика вступает в силу на следующем worktree от `main` без прод-рестарта.
|
||||
- **Ограничение:** Plane-статус **Needs Input** должен существовать на доске проекта (ключ
|
||||
`needs_input` уже в `plane_sync._DEFAULT_STATES`) — инфра-предусловие выполнено для ORCH.
|
||||
- **Открытые проектные вопросы для архитектора (решить в `06-adr/`, НЕ в analysis):**
|
||||
- **DQ-1** — Парковать задачу при Needs Input **автоматически** (`db.set_task_paused` в момент
|
||||
перехода) или оставить park **операторским** (`POST /serial-gate/pause`)? Trade-off:
|
||||
авто-park снимает риск стопора очереди (BR-3), но связывает индикацию (слой B) с осью планировщика.
|
||||
- **DQ-2** — Механизм устаревания `01-questions.md` (BR-4): удалять файл при выпуске полного пакета /
|
||||
приоритет по «вопросы свежее deliverables» (mtime/commit) / явный маркер «answered». Любой выбор
|
||||
обязан быть детерминированным и не зависеть от сетевого Plane.
|
||||
- **DQ-3** — Точное правило приоритета в `_handle_analysis_approved_flow`: проверять
|
||||
`01-questions.md` ДО `files_ok`, либо ввести предикат «вопросы активны» с учётом DQ-2.
|
||||
- **DQ-4** — Коллизия номера `01-questions.md` с `01-brd.md`. Движок читает именно `01-questions.md`
|
||||
(`stage_engine.py:771`) — менять путь = код-изменение; стандарт документирует фактический путь.
|
||||
- **Ограничение по флагам:** новое поведение (приоритет вопросов / авто-park) — под kill-switch с
|
||||
безопасным дефолтом, чтобы откат был байт-в-байт (BR-5).
|
||||
|
||||
## 7. Критерии успеха
|
||||
Аналитик при блокирующей неоднозначности пишет `01-questions.md`, задача надёжно переходит в Needs
|
||||
Input, **не** блокирует serial-gate репо, после ответа заказчика возобновляется и выпускает корректный
|
||||
пакет; при отсутствии вопросов — поведение прежнее. Детальные PASS/FAIL — `03-acceptance-criteria.md`.
|
||||
|
||||
## 8. Риски
|
||||
- Связывание индикации (Plane-статус) с осью планировщика (пауза) при авто-park (DQ-1) — риск
|
||||
непреднамеренного park; смягчение — kill-switch + явный лог.
|
||||
- Устаревший `01-questions.md` зацикливает Needs Input (DQ-2) — смягчение детерминированным
|
||||
supersede-правилом + регресс-тест BR-4.
|
||||
- Регресс serial-gate (ORCH-088/124) при неаккуратной интеграции паузы — смягчение тестами NFR-3.
|
||||
Детали и оценка — `10-tech-risks.md` (заполняет архитектор).
|
||||
111
docs/work-items/ORCH-120/02-trz.md
Normal file
111
docs/work-items/ORCH-120/02-trz.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
work_item: ORCH-120
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 02 — ТЗ (TRZ): ORCH-120 — Открытые вопросы аналитика → Needs Input
|
||||
|
||||
Work Item: **ORCH-120** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
|
||||
> Архитектурное обоснование (выбор механизма приоритета, авто-park vs operator-park, способ
|
||||
> устаревания `01-questions.md`) — задача архитектора (`06-adr/`). Открытые проектные вопросы —
|
||||
> BRD §6 (DQ-1…DQ-4).
|
||||
|
||||
## 1. Сводка изменения
|
||||
|
||||
Активировать и достроить уже существующий, но мёртвый путь «аналитик задаёт блокирующие вопросы →
|
||||
задача в Needs Input». Четыре связанных изменения: (1) **контракт промпта** аналитика +
|
||||
**канон артефакта** `01-questions.md`; (2) **приоритет** ветки вопросов над веткой «файлы готовы» в
|
||||
`_handle_analysis_approved_flow`; (3) **неблокирование serial-gate** через ось «пауза» ORCH-124;
|
||||
(4) **гигиена устаревания** `01-questions.md` на resume-петле. `STAGE_TRANSITIONS`, реестр `QG_CHECKS`,
|
||||
семантика и имена `check_*`, machine-verdict-ключи, схема существующих таблиц — **не меняются**.
|
||||
|
||||
## 2. Задействованные модули / пути
|
||||
|
||||
| Путь | Действие |
|
||||
|------|----------|
|
||||
| `.openclaw/agents/analyst.md` | **изменить** — добавить контракт «блокирующие вопросы → `01-questions.md`, не фабриковать deliverables» (в `<task>` + `<deliverables>` + поведение на resume); сохранить канон 52d (5 секций, 6 полей frontmatter). |
|
||||
| `docs/_templates/01-questions.md` | **создать** — скелет артефакта открытых вопросов (формат: контекст / список блокирующих вопросов с вариантами / что разблокирует анализ). |
|
||||
| `docs/_standards/PIPELINE_DOCS.md` | **изменить** — строка манифеста §2 для `01-questions.md` (владелец `analyst`, категория `when-applicable`, стадия `analysis`, «механизм: ветка Needs Input в `_handle_analysis_approved_flow`», machine-key — «нет, сигнальный»). |
|
||||
| `src/stage_engine.py` | **изменить** — `_handle_analysis_approved_flow`: правило приоритета (вопросы активны → Needs Input до/вместо `files_ok`, см. DQ-3); опц. вызов park (DQ-1); гигиена устаревания (DQ-2). Всё never-raise. |
|
||||
| `src/webhooks/plane.py` | **изменить (точечно)** — `handle_status_start` (analysis-resume ветка, стр. 317–381): при перезапуске аналитика снять паузу (`clear_task_paused`/`POST` эквивалент), чтобы re-enqueued job был claimable. |
|
||||
| `src/db.py` | **переиспользовать** — `set_task_paused` / `clear_task_paused` / `is_task_paused` (ORCH-124, уже есть; новых колонок НЕ вводить). |
|
||||
| `src/serial_gate.py` | **не менять кодом** — ось «пауза» уже исключает `paused_at NOT NULL` (ORCH-124); ORCH-120 лишь корректно её триггерит. |
|
||||
| `src/config.py` | **изменить** — добавить kill-switch(и) нового поведения (напр. `analyst_questions_gate_enabled`, опц. `analyst_needs_input_autopause_enabled`), env `ORCH_*`, безопасные дефолты. |
|
||||
| `src/main.py` | **возможно** — наблюдаемость в блоке `GET /queue` (если потребуется доп. поле); pause/resume эндпоинты ORCH-124 переиспользуются как есть. |
|
||||
|
||||
> Точный набор правок в `src/**` финализирует архитектор (DQ-1…DQ-3). TRZ фиксирует **наблюдаемый
|
||||
> контракт**, а не конкретную реализацию ветвления.
|
||||
|
||||
## 3. Функциональные требования
|
||||
|
||||
### FR-1 — Контракт промпта аналитика (BR-1, BR-6)
|
||||
`.openclaw/agents/analyst.md` явно описывает: при **блокирующей** неоднозначности бизнес-запроса
|
||||
аналитик пишет `docs/work-items/<plane-id>/01-questions.md` (через Write tool) со списком конкретных
|
||||
блокирующих вопросов и **не** выпускает сфабрикованные 4 deliverables. Указывается поведение на
|
||||
перезапуске: прочитать свежие комментарии-ответы в Plane, снять/не переписывать устаревшие вопросы,
|
||||
выпустить полный пакет. Промпт остаётся в каноне 52d (5 секций, 6 полей schema, без `model:`).
|
||||
|
||||
### FR-2 — Приоритет «вопросы активны» (BR-2)
|
||||
В `_handle_analysis_approved_flow` наличие **активных** блокирующих вопросов (`01-questions.md`,
|
||||
с учётом supersede-правила DQ-2) ведёт к `set_issue_needs_input(...)` + комментарий + Telegram +
|
||||
`result.note = "analysis-needs-input"` **независимо** от того, присутствуют ли на диске 4 файла.
|
||||
Сейчас ветка `files_ok` (стр. 711) делает `return` до проверки вопросов (стр. 769) — порядок/предикат
|
||||
исправляется так, что вопросы имеют приоритет. Happy-path (нет вопросов, 4 файла) → `In Review`
|
||||
(`analysis-in-review`) без изменений.
|
||||
|
||||
### FR-3 — Неблокирование serial-gate (BR-3, NFR-3)
|
||||
Переход в Needs Input приводит к тому, что задача **исключается** из «активного» предиката serial-gate
|
||||
(ORCH-088), чтобы следующая задача `orchestrator` могла войти в `analysis`. Механизм — ось «пауза»
|
||||
ORCH-124: `paused_at NOT NULL` уже исключается в `build_claim_clause`/`repo_has_active_task`/
|
||||
`_per_repo_snapshot`. Авто-park vs operator-park — DQ-1. Терминал `{done,cancelled}` и оси
|
||||
`task_deps`/`freeze` не читают `paused_at` — пауза их не обходит (инвариант ORCH-124 цел).
|
||||
|
||||
### FR-4 — Resume + unpark (BR-4)
|
||||
`handle_status_start` (analysis-ветка) при перезапуске аналитика после ответа заказчика снимает паузу
|
||||
(`clear_task_paused`), чтобы re-enqueued analyst-job был claimable (совместно с фиксом ORCH-126 о
|
||||
stale `run_id`/`pid`). Существующий relaunch-guard ORCH-090 (relaunch только для `analysis`) — не
|
||||
ослабляется.
|
||||
|
||||
### FR-5 — Гигиена устаревания `01-questions.md` (BR-4)
|
||||
Перезапущенный аналитик, выпустивший полный валидный пакет (4 файла) **без свежих** блокирующих
|
||||
вопросов, приводит к `In Review`, а не к повторному Needs Input. Реализация supersede — DQ-2
|
||||
(детерминированно, offline, без сетевого Plane).
|
||||
|
||||
### FR-6 — Обратимость / kill-switch (BR-5)
|
||||
Новое поведение под kill-switch с безопасным дефолтом: при отсутствии `01-questions.md` и выключенном
|
||||
под-флаге поток Needs Input/паузы — **байт-в-байт** как до ORCH-120. Скоуп — self-hosting
|
||||
(`orchestrator`); enduro не затронут.
|
||||
|
||||
## 4. Изменения API
|
||||
**Нет новых эндпоинтов.** Переиспользуются существующие `POST /serial-gate/pause` и
|
||||
`POST /serial-gate/resume` (ORCH-124, `src/main.py:396/442`) как операторский путь park/unpark (если
|
||||
архитектор выберет operator-park, DQ-1). При авто-park вызывается `db.set_task_paused` напрямую из
|
||||
движка. Блок `serial_gate` в `GET /queue` уже отдаёт `paused` — возможно дополнение полем-причиной.
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
**Нет.** Колонка `tasks.paused_at` уже введена ORCH-124 (`src/db.py:160`, `_ensure_column`). Новых
|
||||
таблиц/колонок/индексов ORCH-120 не вводит.
|
||||
|
||||
## 6. Требования к новым/изменённым QG checks
|
||||
**Нет.** `01-questions.md` — сигнальный артефакт, **не** machine-verdict-док; гейтом не парсится.
|
||||
`check_analysis_complete` / `check_analysis_approved` / `_parse_*` — байт-в-байт. Поток вопросов
|
||||
остаётся pre-gate-веткой движка (`_handle_analysis_approved_flow`), как и был.
|
||||
|
||||
## 7. Совместимость / регресс
|
||||
- **Обратная совместимость:** при отсутствии `01-questions.md` ветвление `_handle_analysis_approved_flow`
|
||||
и serial-gate работают как прежде (NFR-2). Стадии ≠ `analysis` — не трогаются.
|
||||
- **Kill-switch:** новое поведение (приоритет вопросов / авто-park) выключаемо → откат байт-в-байт
|
||||
(FR-6/BR-5). Область — self-hosting `orchestrator`.
|
||||
- **Инварианты:** ORCH-066 (Needs Input только у аналитика) — не расширяется; ORCH-088/124 (анти-stale-base,
|
||||
терминал/freeze/deps оси) — не регрессируют (NFR-3); never-raise (NFR-1); self-hosting-безопасность
|
||||
(NFR-4: без прод-рестарта/`main`-push).
|
||||
- **Полный регресс** `pytest tests/` остаётся зелёным; обязательный новый регресс-тест (TC-01) красный
|
||||
до фикса и зелёный после.
|
||||
- **Трассировка маркеров (ORCH-078):** правки в `_handle_analysis_approved_flow`/`serial_gate`/
|
||||
`plane.py` сверяются с ADR ORCH-066 / ORCH-088 / ORCH-124 перед изменением (не сломать инварианты).
|
||||
142
docs/work-items/ORCH-120/03-acceptance-criteria.md
Normal file
142
docs/work-items/ORCH-120/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
work_item: ORCH-120
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-120 — Открытые вопросы аналитика → Needs Input
|
||||
|
||||
Work Item: **ORCH-120** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что
|
||||
считается провалом). Reviewer проверяет их буквально по файлам репозитория и тестам.
|
||||
|
||||
---
|
||||
|
||||
## AC-1 — Приоритет вопросов над «файлы готовы» (регресс, обязательный)
|
||||
|
||||
**Условие:** `_handle_analysis_approved_flow` вызывается после аналитика, в worktree присутствуют
|
||||
ОДНОВРЕМЕННО все 4 файла (`01-brd`/`02-trz`/`03-acceptance-criteria`/`04-test-plan.yaml`) И
|
||||
`01-questions.md` с активными блокирующими вопросами.
|
||||
- **PASS:** вызывается `set_issue_needs_input(work_item_id)`, `result.note == "analysis-needs-input"`,
|
||||
`set_issue_in_review` НЕ вызывается, задача НЕ продвигается на `architecture`.
|
||||
- **FAIL:** задача уходит в `In Review` / `architecture` (текущее ошибочное поведение — ветка
|
||||
`files_ok` побеждает).
|
||||
|
||||
---
|
||||
|
||||
## AC-2 — Только вопросы, без deliverables (сохранение существующего поведения)
|
||||
|
||||
**Условие:** `01-questions.md` присутствует, 4 файла отсутствуют.
|
||||
- **PASS:** `set_issue_needs_input` вызван, `result.note == "analysis-needs-input"`, текст вопросов
|
||||
передан в Plane-комментарий и Telegram.
|
||||
- **FAIL:** задача не переходит в Needs Input или падает с исключением.
|
||||
|
||||
---
|
||||
|
||||
## AC-3 — Happy-path без регресса
|
||||
|
||||
**Условие:** `01-questions.md` отсутствует, все 4 файла на месте.
|
||||
- **PASS:** `set_issue_in_review` вызван, `result.note == "analysis-in-review"`, запрошен статус
|
||||
`Approved` (поведение байт-в-байт как до ORCH-120, включая autoApprove-ветку ORCH-089).
|
||||
- **FAIL:** задача ошибочно уходит в Needs Input либо happy-path-комментарий/статус изменился.
|
||||
|
||||
---
|
||||
|
||||
## AC-4 — Needs Input не блокирует serial-gate репо
|
||||
|
||||
**Условие:** задача A `orchestrator` в `analysis` переведена в Needs Input по `01-questions.md`;
|
||||
существует более поздняя задача B того же репо.
|
||||
- **PASS:** A исключена из «активного» предиката serial-gate (через `paused_at NOT NULL`, ось
|
||||
ORCH-124); B может войти в `analysis` (claim analyst-job B не блокируется задачей A).
|
||||
- **FAIL:** A продолжает держать FIFO репо закрытым (`repo_has_active_task` возвращает True из-за A),
|
||||
B не может стартовать, пока заказчик не ответит.
|
||||
|
||||
---
|
||||
|
||||
## AC-5 — Resume снимает паузу и перезапускает аналитика
|
||||
|
||||
**Условие:** заказчик ответил и вернул issue в рабочий статус (In Progress / To Analyse) на стадии
|
||||
`analysis`; активного job нет.
|
||||
- **PASS:** `handle_status_start` снимает паузу (`paused_at` → NULL) и enqueue'ит analyst-job; job
|
||||
забирается из очереди (не застревает со stale `run_id`/`pid`, ср. ORCH-126); relaunch-guard ORCH-090
|
||||
(только `analysis`) соблюдён.
|
||||
- **FAIL:** задача остаётся paused (job не claimable) ИЛИ перезапуск происходит на не-`analysis` стадии
|
||||
(нарушение ORCH-090).
|
||||
|
||||
---
|
||||
|
||||
## AC-6 — Гигиена устаревшего `01-questions.md`
|
||||
|
||||
**Условие:** перезапущенный аналитик выпустил полный валидный пакет (4 файла) без свежих блокирующих
|
||||
вопросов; устаревший `01-questions.md` от прошлого прогона мог остаться.
|
||||
- **PASS:** задача переходит в `In Review` (`analysis-in-review`), а НЕ повторно в Needs Input;
|
||||
supersede-правило (DQ-2) применено детерминированно и offline.
|
||||
- **FAIL:** устаревший `01-questions.md` повторно роняет задачу в Needs Input (бесконечная петля).
|
||||
|
||||
---
|
||||
|
||||
## AC-7 — Контракт промпта аналитика (анти-дрейф)
|
||||
|
||||
**Условие:** содержимое `.openclaw/agents/analyst.md`.
|
||||
- **PASS:** промпт документирует канал `01-questions.md` (блокирующие вопросы → Needs Input, не
|
||||
фабриковать deliverables) И сохраняет канон 52d (5 XML-секций, 6 полей frontmatter-схемы, без
|
||||
`model:`); `tests/test_agent_prompts_canon.py` зелёный, добавлен assert наличия контракта вопросов.
|
||||
- **FAIL:** канал не документирован, либо нарушен канон 52d, либо тест канона красный.
|
||||
|
||||
---
|
||||
|
||||
## AC-8 — Канон артефакта `01-questions.md`
|
||||
|
||||
**Условие:** наличие скелета и записи в манифесте.
|
||||
- **PASS:** `docs/_templates/01-questions.md` существует; `docs/_standards/PIPELINE_DOCS.md` содержит
|
||||
строку манифеста для `01-questions.md` (владелец `analyst`, категория `when-applicable`, механизм
|
||||
«ветка Needs Input», не machine-verdict).
|
||||
- **FAIL:** скелет или строка манифеста отсутствуют.
|
||||
|
||||
---
|
||||
|
||||
## AC-9 — Обратимость / нулевая регрессия
|
||||
|
||||
**Условие:** kill-switch нового поведения выключен ИЛИ репо вне self-hosting-области (enduro-trails).
|
||||
- **PASS:** ветвление `_handle_analysis_approved_flow` и serial-gate работают **байт-в-байт** как до
|
||||
ORCH-120; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД не изменены.
|
||||
- **FAIL:** обнаружено отличие поведения при выключенном флаге / для enduro.
|
||||
|
||||
---
|
||||
|
||||
## AC-10 — never-raise / self-hosting-безопасность
|
||||
|
||||
**Условие:** сбой новой логики (ошибка чтения файла, park-вызова, определения приоритета).
|
||||
- **PASS:** `advance_stage`/launcher не падает, деградирует к безопасному прежнему поведению + WARNING;
|
||||
поток не деплоит, не рестартит прод-контейнер, не пушит в `main`.
|
||||
- **FAIL:** исключение всплывает наружу / встаёт конвейер / затронут прод/`main`.
|
||||
|
||||
---
|
||||
|
||||
## AC-11 — Полный регресс зелёный
|
||||
|
||||
**Условие:** `pytest tests/ -q`.
|
||||
- **PASS:** вся сюита зелёная; новый обязательный регресс-тест (AC-1 / TC-01) красный на коде до фикса
|
||||
и зелёный после.
|
||||
- **FAIL:** любой тест красный, либо регресс-тест проходит и на дофиксовом коде (не доказывает баг).
|
||||
|
||||
---
|
||||
|
||||
## Сводная матрица AC ↔ FR/BR
|
||||
| AC | Покрывает |
|
||||
|----|-----------|
|
||||
| AC-1 | BR-2 / FR-2 (регресс) |
|
||||
| AC-2 | BR-2 / FR-2 |
|
||||
| AC-3 | BR-5 / FR-2 |
|
||||
| AC-4 | BR-3 / FR-3 |
|
||||
| AC-5 | BR-4 / FR-4 |
|
||||
| AC-6 | BR-4 / FR-5 |
|
||||
| AC-7 | BR-1 / BR-6 / FR-1 |
|
||||
| AC-8 | BR-6 / FR-1 |
|
||||
| AC-9 | BR-5 / FR-6 / NFR-2 |
|
||||
| AC-10 | NFR-1 / NFR-4 |
|
||||
| AC-11 | NFR-2 |
|
||||
88
docs/work-items/ORCH-120/04-test-plan.yaml
Normal file
88
docs/work-items/ORCH-120/04-test-plan.yaml
Normal file
@@ -0,0 +1,88 @@
|
||||
work_item: ORCH-120
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
title: "Аналитик: открытые вопросы → Needs Input (приоритет, неблокирование serial-gate, resume)"
|
||||
framework: pytest
|
||||
scope: >
|
||||
Покрывает поток «блокирующие открытые вопросы аналитика → Needs Input»:
|
||||
приоритет ветки вопросов над «файлы готовы» (_handle_analysis_approved_flow),
|
||||
неблокирование per-repo serial-gate (ось паузы ORCH-124), resume+unpark,
|
||||
гигиена устаревшего 01-questions.md, контракт промпта и канон артефакта,
|
||||
never-raise и нулевая регрессия. Вне покрытия: расширение Needs Input на
|
||||
других агентов, новые QG/рёбра STAGE_TRANSITIONS, авто-ответ на вопросы.
|
||||
notes: >
|
||||
TC-01 — ОБЯЗАТЕЛЬНЫЙ регресс-тест: красный на коде ДО фикса (ветка files_ok
|
||||
побеждает → In Review), зелёный ПОСЛЕ. Тесты движка прогоняют
|
||||
_handle_analysis_approved_flow напрямую (launcher-путь), мокая plane_sync-
|
||||
сеттеры и используя временный worktree (паттерн tests/test_analyst_status_only_regression.py
|
||||
и tests/test_auto_approve_brd.py). Полный регресс pytest tests/ остаётся зелёным.
|
||||
|
||||
tests:
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "РЕГРЕСС: 4 файла + активный 01-questions.md одновременно -> set_issue_needs_input вызван, note=='analysis-needs-input', set_issue_in_review НЕ вызван (приоритет вопросов, AC-1). Красный до фикса."
|
||||
module: tests/test_orch120_analyst_needs_input.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "01-questions.md есть, 4 файлов нет -> Needs Input, текст вопросов в Plane-комментарии и Telegram (AC-2, сохранение существующего поведения)."
|
||||
module: tests/test_orch120_analyst_needs_input.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "Happy-path: нет 01-questions.md, 4 файла на месте -> set_issue_in_review, note=='analysis-in-review', запрос статуса Approved (AC-3, нет регресса)."
|
||||
module: tests/test_orch120_analyst_needs_input.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: integration
|
||||
description: "Задача A в analysis переведена в Needs Input (paused_at NOT NULL) -> serial_gate исключает её из активного предиката; задача B того же репо может войти в analysis (AC-4, неблокирование FIFO)."
|
||||
module: tests/test_orch120_serial_gate_needs_input.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: integration
|
||||
description: "Resume: возврат issue в рабочий статус на analysis при отсутствии активного job -> handle_status_start снимает паузу (paused_at->NULL) и enqueue'ит analyst-job; relaunch-guard ORCH-090 (только analysis) соблюдён (AC-5)."
|
||||
module: tests/test_orch120_resume_unpark.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "Гигиена устаревания: перезапущенный аналитик выпустил полный валидный пакет без свежих вопросов -> In Review, НЕ повторный Needs Input; supersede-правило детерминировано и offline (AC-6)."
|
||||
module: tests/test_orch120_analyst_needs_input.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "Анти-дрейф промпта: .openclaw/agents/analyst.md документирует канал 01-questions.md (блокирующие вопросы -> Needs Input, не фабриковать deliverables) и сохраняет канон 52d (5 секций, 6 полей, без model:) (AC-7)."
|
||||
module: tests/test_agent_prompts_canon.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "Канон артефакта: docs/_templates/01-questions.md существует и docs/_standards/PIPELINE_DOCS.md содержит строку манифеста для 01-questions.md (владелец analyst, when-applicable, не machine-verdict) (AC-8)."
|
||||
module: tests/test_orch120_questions_artifact_canon.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: "never-raise: сбой новой логики (ошибка чтения 01-questions.md / park-вызова) не роняет advance_stage, деградирует к безопасному прежнему поведению + WARNING (AC-10)."
|
||||
module: tests/test_orch120_analyst_needs_input.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "Обратимость: kill-switch выключен ИЛИ репо вне self-hosting (enduro-trails) -> ветвление _handle_analysis_approved_flow и serial-gate байт-в-байт как до ORCH-120 (AC-9)."
|
||||
module: tests/test_orch120_analyst_needs_input.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: integration
|
||||
description: "Полный регресс pytest tests/ зелёный; STAGE_TRANSITIONS/QG_CHECKS/check_* снапшот не изменён (AC-11, NFR-2)."
|
||||
module: tests/test_stage_transitions_snapshot.py
|
||||
expected: PASS
|
||||
@@ -0,0 +1,248 @@
|
||||
---
|
||||
work_item: ORCH-120
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# ADR-001: Открытые вопросы аналитика → Needs Input (приоритет, неблокирование serial-gate, resume)
|
||||
|
||||
Work Item: **ORCH-120** · Repo: **orchestrator** (self-hosting) · Стадия: architecture
|
||||
Связь: BRD `01-brd.md`, ТЗ `02-trz.md`, AC `03-acceptance-criteria.md`, план тестов `04-test-plan.yaml`, риски `10-tech-risks.md`.
|
||||
Сквозная регистрация: `docs/architecture/adr/adr-0053-analyst-open-questions-needs-input-flow.md`.
|
||||
|
||||
## Статус
|
||||
Proposed
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
Задача — баг (`BUG:` в заголовке), **эскалированный в полный цикл** (`escalate: full-cycle`,
|
||||
ADR-001 D5 ORCH-019): фикс требует архитектурных решений, поэтому выпущен полный analysis-пакет и
|
||||
задача идёт через стадию `architecture`. Аналитик передал 4 открытых проектных вопроса (BRD §6:
|
||||
DQ-1…DQ-4), которые и решаются этим ADR.
|
||||
|
||||
**Корень (верифицировано в коде).** Механизм «аналитик задаёт блокирующие вопросы → задача в
|
||||
Needs Input» в движке **уже есть, но мёртв**: `src/stage_engine.py::_handle_analysis_approved_flow`
|
||||
(ветка `01-questions.md`, стр. 769–786) читает файл и вызывает `set_issue_needs_input(...)` +
|
||||
коммент в Plane + Telegram. Четыре смежных дефекта делают его нерабочим:
|
||||
|
||||
1. **Контракт не доведён до аналитика.** `.openclaw/agents/analyst.md` нигде не упоминает
|
||||
`01-questions.md` (ни в `<task>`, ни в `<deliverables>`); скелета `docs/_templates/01-questions.md`
|
||||
нет; в `docs/_standards/PIPELINE_DOCS.md` артефакт не описан. Аналитик не знает о канале и
|
||||
**домысливает** требования, чтобы сдать обязательные 4 файла.
|
||||
2. **Ошибка приоритета веток.** Ветка `files_ok` (`check_analysis_complete` — все 4 файла на месте,
|
||||
стр. 711–767) проверяется **первой** и делает `return`; ветка `01-questions.md` (стр. 769)
|
||||
достижима только если 4 файла НЕ полны. Сфабрикованный полный пакет уходит в `In Review`,
|
||||
игнорируя блокирующие вопросы.
|
||||
3. **Needs Input клинит serial-gate репо.** Задача в Needs Input остаётся в `stage='analysis'`
|
||||
(Plane-статус — слой B индикации, ORCH-066, **не** меняет `tasks.stage`) и `paused_at IS NULL`.
|
||||
По правилу ORCH-088 такая «активная» задача держит FIFO репо закрытым до ответа человека
|
||||
(часы/дни) — ни одна следующая задача `orchestrator` не входит в `analysis`.
|
||||
4. **Нет гигиены устаревшего `01-questions.md`.** После ответа заказчика `handle_status_start`
|
||||
(`src/webhooks/plane.py:261`, analysis-resume) перезапускает аналитика; если тот выпускает
|
||||
полный пакет, старый `01-questions.md` остаётся в ветке. Без правила supersede он либо
|
||||
игнорируется, либо вечно роняет задачу в Needs Input.
|
||||
|
||||
**Что НЕ нужно строить с нуля** (BRD §6 «Допущение»): `set_issue_needs_input`, чтение файла,
|
||||
ось «пауза» (`tasks.paused_at` + `db.set_task_paused`/`clear_task_paused`/`is_task_paused`,
|
||||
ORCH-124), эндпоинты `POST /serial-gate/pause|resume` — **уже существуют**. ORCH-120 **активирует
|
||||
и достраивает** путь, а не изобретает его.
|
||||
|
||||
**Предусловия выполнены вне объёма:** ORCH-124 (ось «пауза без блокировки» — фундамент BR-3),
|
||||
ORCH-126 (queued-job не застревает со stale `run_id`/`pid` — гарантирует claim перезапущенного
|
||||
analyst-job). Plane-статус **Needs Input** существует (ключ `needs_input` в `_DEFAULT_STATES`).
|
||||
|
||||
---
|
||||
|
||||
## Решение (сводка)
|
||||
|
||||
Активировать мёртвый путь четырьмя согласованными изменениями, **аддитивно, под kill-switch,
|
||||
скоуп self-hosting, never-raise**: (D1) контракт промпта + (D2) канон артефакта `01-questions.md`;
|
||||
(D3) **приоритет** ветки вопросов над `files_ok` с детерминированным supersede; (D4) **авто-park**
|
||||
при Needs Input через ось «пауза» ORCH-124; (D5) **resume + unpark** в `handle_status_start`.
|
||||
`STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS` / семантика `check_analysis_complete` /
|
||||
`check_analysis_approved` / machine-verdict-ключи / схемы существующих таблиц — **байт-в-байт не
|
||||
тронуты** (поток — pre-gate-ветка движка, **не** Quality Gate; артефакт — сигнальный, **не**
|
||||
machine-verdict). Это решение DQ-1…DQ-4.
|
||||
|
||||
---
|
||||
|
||||
## Решения по открытым вопросам (DQ-1…DQ-4)
|
||||
|
||||
### DQ-1 — авто-park vs operator-park при Needs Input → **авто-park, config-gated**
|
||||
|
||||
**Решение.** В момент перехода в Needs Input движок вызывает `db.set_task_paused(task_id)`
|
||||
**автоматически** (под под-флагом `analyst_needs_input_autopause_enabled`, скоуп self-hosting),
|
||||
а на resume — `db.clear_task_paused(task_id)` (D5). Operator-park (`POST /serial-gate/pause`)
|
||||
сохраняется как ручной fallback, но **не** обязателен для BR-3.
|
||||
|
||||
**Обоснование.** BR-3 и весь эпик ORCH-088 («10–20 задач за ночь», автономный пакетный прогон)
|
||||
требуют, чтобы Needs Input **не** клинил очередь. Operator-park вводит человеческий шаг в каждую
|
||||
итерацию — это противоречит цели автономности: пока оператор не нажал pause, FIFO репо закрыт.
|
||||
Авто-park снимает блокировку детерминированно и без человека.
|
||||
|
||||
**Снятие риска связывания индикации (слой B) с осью планировщика** (явный риск из BRD §6 DQ-1):
|
||||
- Триггер узкий и детерминированный — **только** переход аналитика в Needs Input в
|
||||
`_handle_analysis_approved_flow`, а не общее правило «статус → пауза».
|
||||
- Отдельный под-флаг `analyst_needs_input_autopause_enabled` (независимый от questions-gate),
|
||||
дефолт `True` в self-hosting-области → откат до operator-park одним флагом.
|
||||
- never-raise: `set_task_paused` уже возвращает `False` на ошибке; сбой park **не** отменяет
|
||||
переход в Needs Input (деградация к operator-park) и не роняет `advance_stage`.
|
||||
- Симметричный unpark на resume (D5) исключает «застрявшую паузу».
|
||||
|
||||
**Инвариант ORCH-124 не нарушен:** `paused_at` исключает задачу только из оси «активная задача»
|
||||
serial-gate; оси `task_deps`/`repo_freeze`/терминал `{done,cancelled}` `paused_at` **не читают** —
|
||||
пауза их не обходит (`src/serial_gate.py:33` нормативно).
|
||||
|
||||
### DQ-2 — устаревание `01-questions.md` (BR-4) → **freshness-gated supersede (mtime), детерминированно и offline**
|
||||
|
||||
**Решение.** Ввести чистый предикат `_questions_active(worktree_path, work_item_id, files_ok) -> bool`
|
||||
в `stage_engine` (или leaf-хелпер), детерминированный и offline (только filesystem, без сети/git):
|
||||
|
||||
- `01-questions.md` отсутствует → **не активны** (`False`).
|
||||
- Пакет **неполон** (`files_ok == False`) и файл присутствует → **активны** (`True`): вопросы есть,
|
||||
deliverables нет — приоритет вопросов (AC-2).
|
||||
- Пакет **полон** (`files_ok == True`) и файл присутствует → сверка свежести:
|
||||
**superseded ⇔ все 4 deliverables строго новее `01-questions.md`** (по `os.path.getmtime`).
|
||||
`superseded == True` → **не активны** (`False`, → In Review, AC-6); иначе (вопросы не старше
|
||||
новейшего deliverable) → **активны** (`True`, → Needs Input, AC-1).
|
||||
|
||||
**Почему mtime-freshness, а не альтернативы.** Кандидаты и причины отказа:
|
||||
- **Удаление файла аналитиком** — у аналитика нет Delete-tool (только Write `docs/...`); полагаться
|
||||
на «забыл удалить» нельзя; AC-6 явно допускает «файл **мог остаться** нетронутым».
|
||||
- **Контент-маркер `status: resolved`** — требует, чтобы аналитик **переписал** файл; при
|
||||
«файл остался нетронутым» (AC-6) маркера нет → ложный Needs Input. Плюс вводит парсинг
|
||||
машинного ключа в сигнальный артефакт (трение с BR-6).
|
||||
- **Git-recency коммитов** — надёжно в проде, но юнит-тесты (`04-test-plan.yaml`: временный worktree
|
||||
с plain-файлами, паттерн `test_auto_approve_brd.py`) **не коммитят** → нетестируемо/хрупко.
|
||||
- **mtime-freshness** — единственный механизм, который (а) тестируем на plain-файлах (тест задаёт
|
||||
порядок записи/mtime), (б) offline и без нового парсинга, (в) **не зависит от действия LLM**:
|
||||
на полном прогоне аналитик в любом случае пишет 4 deliverables (свежий mtime), поэтому
|
||||
оставленный нетронутым старый `01-questions.md` автоматически superseded — ровно сценарий AC-6.
|
||||
|
||||
**Контракт промпта (D1) дополняет, не заменяет** механизм: на resume аналитик, у которого остались
|
||||
блокеры, **перезаписывает** `01-questions.md` (свежий mtime → снова активен); при полном ответе —
|
||||
просто пишет 4 deliverables, freshness supersede’ит старые вопросы. Так оба слоя согласованы.
|
||||
|
||||
**Направление fail (never-raise, NFR-1) для DQ-2:**
|
||||
- Ошибка `getmtime`/сверки при **доказанно существующем** `01-questions.md` → считать вопросы
|
||||
**активными** (Needs Input) — безопасно для цели «не строить на домыслах».
|
||||
- Катастрофический сбой (не можем определить даже наличие файла) → деградация к **прежнему**
|
||||
поведению (`files_ok` → In Review) + WARNING.
|
||||
|
||||
### DQ-3 — точное правило приоритета в `_handle_analysis_approved_flow` → **вопросы проверяются ДО `files_ok`, под kill-switch**
|
||||
|
||||
**Решение.** Реструктурировать тело (только при `applies(repo)` и `analyst_questions_gate_enabled`):
|
||||
|
||||
```
|
||||
files_ok, _ = QG_CHECKS["check_analysis_complete"](repo, work_item_id, branch)
|
||||
questions_active = _questions_active(worktree, work_item_id, files_ok) # D2, never-raise
|
||||
if questions_active:
|
||||
set_issue_needs_input(work_item_id)
|
||||
<Plane-коммент с текстом вопросов> + <Telegram> # как сейчас (стр. 770-784)
|
||||
if analyst_needs_input_autopause_enabled and applies(repo):
|
||||
db.set_task_paused(task_id) # D4 авто-park, never-raise
|
||||
result.note = "analysis-needs-input"
|
||||
return
|
||||
if files_ok:
|
||||
<ветка In Review + autoApprove ORCH-089> # существующая, байт-в-байт
|
||||
return
|
||||
<ветка "ни файлов, ни вопросов"> # существующая
|
||||
```
|
||||
|
||||
- **Приоритет вопросов > «файлы на месте»** (AC-1): `questions_active` проверяется первым.
|
||||
- **Happy-path** (нет файла вопросов): `questions_active == False` → ветка `files_ok` → In Review,
|
||||
включая autoApprove ORCH-089 — **байт-в-байт** (AC-3).
|
||||
- **Kill-switch / out-of-scope** (`analyst_questions_gate_enabled == False` ИЛИ репо вне области):
|
||||
исполняется **исходный** порядок (`files_ok` первым; затем плоский `os.path.isfile(questions_path)`
|
||||
— существующая ветка) — **байт-в-байт как до ORCH-120**, включая для enduro (AC-9).
|
||||
|
||||
### DQ-4 — коллизия номера `01-questions.md` с `01-brd.md` → **сохранить путь `01-questions.md`**
|
||||
|
||||
**Решение.** Движок уже читает именно `docs/work-items/<wid>/01-questions.md`
|
||||
(`stage_engine.py:771`) — это **рабочий** контракт; смена пути = код-изменение + поломка читателя
|
||||
без выгоды. Путь **сохраняется**. В `PIPELINE_DOCS.md` (D2) фиксируется нормативно: префикс `01-` —
|
||||
общий для артефактов стадии `analysis` аналитика; `01-brd.md` — обязательный deliverable,
|
||||
`01-questions.md` — **сигнальный** when-applicable артефакт того же владельца/стадии; коллизии нет,
|
||||
т.к. файлы разноимённые, гейт `check_analysis_complete` проверяет ровно `01-brd.md`/`02`/`03`/`04`
|
||||
(`01-questions.md` им не парсится).
|
||||
|
||||
---
|
||||
|
||||
## Изменения (карта, нормативно)
|
||||
|
||||
| Путь | Действие | Примечание |
|
||||
|------|----------|------------|
|
||||
| `.openclaw/agents/analyst.md` | изменить | D1: контракт «блокирующие вопросы → `01-questions.md`, НЕ фабриковать 4 deliverables» в `<task>`+`<deliverables>`+поведение на resume; **сохранить канон 52d** (5 секций, 6 полей, без `model:`). |
|
||||
| `docs/_templates/01-questions.md` | создать | D2: скелет (контекст / список блокирующих вопросов с вариантами / «что разблокирует анализ»). |
|
||||
| `docs/_standards/PIPELINE_DOCS.md` | изменить | D2: строка манифеста §2 (`01-questions.md`, владелец `analyst`, `when-applicable`, стадия `analysis`, механизм «ветка Needs Input в `_handle_analysis_approved_flow`», machine-key — «нет, сигнальный») + примечание о префиксе `01-` (DQ-4). |
|
||||
| `src/stage_engine.py` | изменить | D3: приоритет + `_questions_active` (D2) + авто-park (D4); всё never-raise, под kill-switch. |
|
||||
| `src/webhooks/plane.py` | изменить (точечно) | D5: analysis-resume ветка `handle_status_start` (стр. 317–381) — `clear_task_paused(task_id)` (под autopause-флагом + `applies`), never-raise. |
|
||||
| `src/config.py` | изменить | 3 новых ключа (ниже), безопасные дефолты. |
|
||||
| `src/db.py` | переиспользовать | `set_task_paused`/`clear_task_paused` (ORCH-124) — **новых колонок нет**. |
|
||||
| `src/serial_gate.py` | **не менять кодом** | ось «пауза» уже исключает `paused_at NOT NULL` (ORCH-124); ORCH-120 лишь корректно её триггерит (D4/D5). |
|
||||
|
||||
**Флаги (`src/config.py`), дефолты безопасны:**
|
||||
|
||||
| Ключ | Env | Дефолт | Назначение |
|
||||
|------|-----|--------|------------|
|
||||
| `analyst_questions_gate_enabled` | `ORCH_ANALYST_QUESTIONS_GATE_ENABLED` | `True` | kill-switch приоритета+supersede (D3). `False` → исходный порядок байт-в-байт. |
|
||||
| `analyst_questions_gate_repos` | `ORCH_ANALYST_QUESTIONS_GATE_REPOS` | `""` | CSV; **пусто → self-hosting only** (`is_self_hosting_repo`, как ORCH-35/43/58). enduro не затронут. |
|
||||
| `analyst_needs_input_autopause_enabled` | `ORCH_ANALYST_NEEDS_INPUT_AUTOPAUSE_ENABLED` | `True` | независимый под-тумблер авто-park/unpark (D4/D5). `False` → operator-park (`POST /serial-gate/pause`). |
|
||||
|
||||
`applies(repo)` (локально, без сети) проверяется **первым** — выключенный флаг / вне области
|
||||
стоит ноль сетевых вызовов и даёт байт-в-байт прежнее ветвление (зеркало `serial_gate.applies`).
|
||||
|
||||
---
|
||||
|
||||
## Изменения схемы БД
|
||||
**Нет.** Колонка `tasks.paused_at` введена ORCH-124 (`src/db.py:160`, `_ensure_column`). Новых
|
||||
таблиц/колонок/индексов ORCH-120 не вводит. Поэтому отдельный `08-data-requirements.md` не выпускается.
|
||||
|
||||
## Инфраструктурные требования
|
||||
**Нет нового.** Plane-статус **Needs Input** уже на доске (ключ `needs_input` в
|
||||
`plane_sync._DEFAULT_STATES`); эндпоинты `POST /serial-gate/pause|resume` существуют (ORCH-124).
|
||||
Поэтому отдельный `07-infra-requirements.md` не выпускается.
|
||||
|
||||
## QG / стадии
|
||||
**Не тронуты.** `01-questions.md` — сигнальный, не machine-verdict (BR-6). `check_analysis_complete`/
|
||||
`check_analysis_approved`/`_parse_*`/`STAGE_TRANSITIONS`/`QG_CHECKS` — байт-в-байт (NFR-2). Поток
|
||||
вопросов остаётся pre-gate-веткой движка.
|
||||
|
||||
---
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы.**
|
||||
- Аналитик получает легитимный канал «не знаю — спрошу» вместо принуждения к фабрикации (BR-1);
|
||||
конвейер перестаёт строить решения поверх домыслов.
|
||||
- Блокирующие вопросы надёжно достигают Needs Input даже при частичных deliverables (BR-2/AC-1).
|
||||
- serial-gate репо не клинит на задаче, ждущей человека (BR-3/AC-4) — автономный пакетный прогон
|
||||
(ORCH-088) не стопорится.
|
||||
- Resume-петля корректна: ответ → unpark → claim перезапущенного analyst-job (BR-4/AC-5).
|
||||
- Устаревший `01-questions.md` детерминированно supersede’ится без зависимости от LLM (AC-6).
|
||||
- Полная обратимость: 3 флага с безопасными дефолтами; вне области/выключено → байт-в-байт (AC-9).
|
||||
|
||||
**Минусы / ограничения.**
|
||||
- Авто-park связывает индикацию (слой B) с осью планировщика — смягчено узким триггером,
|
||||
отдельным флагом, never-raise и симметричным unpark (DQ-1).
|
||||
- mtime-freshness теоретически хрупок при экзотической re-материализации worktree — на практике
|
||||
устойчив (полный прогон всегда пишет свежие deliverables); смягчён fail-в-сторону-Needs-Input и
|
||||
контрактом промпта (DQ-2, см. `10-tech-risks.md` TR-2).
|
||||
- Needs Input по-прежнему **только** у аналитика (ORCH-066 BR-10 не расширяется) — намеренно.
|
||||
|
||||
**Трассировка маркеров (ORCH-078).** Правки в `_handle_analysis_approved_flow`/`handle_status_start`
|
||||
сверены с ADR ORCH-066 (Needs-Input владение), ORCH-088/124 (serial-gate/пауза), ORCH-089
|
||||
(autoApprove), ORCH-090 (relaunch-guard) — инварианты не сломаны. Блок `_handle_analysis_approved_flow`
|
||||
несёт 3+ маркера → эволюция агрегирована в сквозном `adr-0053`.
|
||||
|
||||
## Соответствие AC
|
||||
AC-1 → D3 (приоритет) + D2 (active при files_ok). AC-2 → D3 (questions при !files_ok). AC-3 → D3
|
||||
(happy-path байт-в-байт). AC-4 → D4 (авто-park → `paused_at` исключает из активного предиката).
|
||||
AC-5 → D5 (resume unpark). AC-6 → D2 (freshness supersede). AC-7 → D1 (контракт промпта). AC-8 → D2
|
||||
(скелет + манифест). AC-9 → флаги/скоуп (байт-в-байт off/enduro). AC-10 → never-raise во всех врезках.
|
||||
AC-11 → снапшот `STAGE_TRANSITIONS`/`QG_CHECKS` не изменён.
|
||||
41
docs/work-items/ORCH-120/10-tech-risks.md
Normal file
41
docs/work-items/ORCH-120/10-tech-risks.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
work_item: ORCH-120
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 10 — Технические риски: ORCH-120 — Открытые вопросы аналитика → Needs Input
|
||||
|
||||
Work Item: **ORCH-120** · Repo: **orchestrator** (self-hosting) · Стадия: architecture
|
||||
|
||||
> Информационный (гейтом не парсится). Перечисляет риски реализации и их митигейшн.
|
||||
|
||||
## Реестр рисков
|
||||
|
||||
| ID | Риск | Вер. | Влия. | Митигейшн |
|
||||
|----|------|------|-------|-----------|
|
||||
| TR-1 | **Связывание индикации (слой B) с осью планировщика** при авто-park (DQ-1): непреднамеренный park из-за широкого/ошибочного триггера. | Низ. | Сред. | Триггер узкий — **только** переход аналитика в Needs Input в `_handle_analysis_approved_flow`, не общее «статус→пауза». Отдельный под-флаг `analyst_needs_input_autopause_enabled` (откат к operator-park). never-raise: сбой `set_task_paused` не отменяет Needs Input. Симметричный unpark на resume (D5) исключает «застрявшую паузу». |
|
||||
| TR-2 | **Хрупкость mtime-freshness** (DQ-2): re-материализация worktree выставляет близкие mtime → ложный supersede (AC-1 ломается) или ложная активность (AC-6 ломается). | Низ. | Сред. | Полный прогон аналитика **всегда** пишет 4 deliverables свежим mtime → старый нетронутый `01-questions.md` детерминированно старше (AC-6 устойчив). Сверка строгая (`>` для всех 4). Контракт промпта (D1): на resume с блокерами аналитик **перезаписывает** вопросы → свежий mtime (AC-1 устойчив). Fail-в-сторону-Needs-Input при ошибке `getmtime` на существующем файле. Покрыто TC-01/TC-06. |
|
||||
| TR-3 | **Регресс serial-gate** (ORCH-088/124): неаккуратная интеграция паузы обходит freeze/deps или ломает анти-stale-base. | Низ. | Выс. | `serial_gate.py`/`task_deps.py`/`stages.py` **кодом не трогаются** — пауза уже ортогональна (ORCH-124: `paused_at` не читают оси freeze/deps/терминал). Анти-stale-base цел: нормальная задача `paused_at IS NULL` держит гейт; на resume свежесть базы дают существующие механизмы (отложенный срез / `auto_rebase_onto_main`). Снапшот-тест serial-gate + TC-04. |
|
||||
| TR-4 | **Бесконечная петля Needs Input** при устаревшем `01-questions.md` (если supersede не сработал). | Низ. | Сред. | Детерминированный freshness supersede (DQ-2) + обязательный регресс TC-06 (полный пакет без свежих вопросов → In Review, не повторный Needs Input). Контракт промпта подкрепляет. |
|
||||
| TR-5 | **Застрявшая пауза на resume** (task остаётся `paused_at NOT NULL` после ответа → семантика «активна, но помечена paused»). | Низ. | Низ. | D5: `clear_task_paused` на analysis-resume ветке `handle_status_start` (под autopause-флагом + `applies`), идемпотентно/never-raise. Ручной fallback — `POST /serial-gate/resume`. Покрыто TC-05. |
|
||||
| TR-6 | **Нарушение never-raise** новой логики (чтение файла/park/приоритет) роняет `advance_stage`/launcher → встаёт конвейер всех проектов (self-hosting). | Низ. | Выс. | Все врезки в `try/except` с деградацией к **прежнему** поведению + WARNING (паттерн `serial_gate`/`labels`/`cancel`). `set_task_paused`/`clear_task_paused` уже never-raise (→ `False`). Покрыто TC-09. |
|
||||
| TR-7 | **Регресс enduro / нулевой-флаг** (поведение отличается при выключенном kill-switch или вне self-hosting). | Низ. | Сред. | `applies(repo)` первым; off/out-of-scope → исходный порядок (`files_ok` первым + плоский isfile) **байт-в-байт**. enduro-аналитик `01-questions.md` не эмитит. Покрыто TC-10 + снапшот TC-11. |
|
||||
| TR-8 | **Дрейф промпта** (контракт вопросов не задокументирован или сломан канон 52d). | Низ. | Низ. | Анти-дрейф `tests/test_agent_prompts_canon.py` (5 секций, 6 полей, без `model:`) + новый assert наличия контракта вопросов (TC-07); канон артефакта — TC-08. |
|
||||
|
||||
## Сводный вывод
|
||||
|
||||
Доминирующий класс — **корректность интеграции с serial-gate (ORCH-088/124) и never-raise на
|
||||
self-hosting горячем пути** (TR-3/TR-6: высокое влияние, низкая вероятность). Оба полностью
|
||||
структурно нейтрализованы: serial-gate кодом не трогается (пауза уже ортогональна), все врезки
|
||||
изолированы и деградируют к прежнему поведению. Остальные риски — низкого влияния и покрыты
|
||||
обязательными регресс-тестами (TC-01/TC-04/TC-05/TC-06/TC-09/TC-10).
|
||||
|
||||
Остаточный риск для прод-конвейера (self-hosting) — **низкий**: изменение аддитивно, под тремя
|
||||
флагами с безопасными дефолтами (байт-в-байт откат), не деплоит / не рестартит прод / не пушит в
|
||||
`main` / не трогает detached-процессы (NFR-4). Эскалация `arch:major-change` **не требуется**
|
||||
(нет новой стадии/QG/компонента/смены БД); возврат в анализ **не требуется** (ТЗ удовлетворимо без
|
||||
нарушения принципов архитектуры).
|
||||
105
docs/work-items/ORCH-120/12-review.md
Normal file
105
docs/work-items/ORCH-120/12-review.md
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
verdict: APPROVED # APPROVED | REQUEST_CHANGES — строго одно из двух, UPPERCASE
|
||||
work_item: ORCH-120
|
||||
stage: review
|
||||
author_agent: reviewer
|
||||
status: approved
|
||||
created_at: 2026-06-17
|
||||
model_used: claude-opus-4-8
|
||||
type: review
|
||||
work_item_id: ORCH-120
|
||||
version: 2
|
||||
---
|
||||
|
||||
# Review ORCH-120 — Открытые вопросы аналитика → Needs Input
|
||||
|
||||
## Summary
|
||||
|
||||
Реализация **сильная, завершённая и корректная**. Ранее **мёртвый** путь «аналитик задаёт
|
||||
блокирующие вопросы → `01-questions.md` → Needs Input» активирован четырьмя согласованными
|
||||
изменениями (контракт промпта + канон артефакта; приоритет вопросов над `files_ok`; авто-park через
|
||||
ось «пауза» ORCH-124; resume + unpark). Чистая логика вынесена в leaf `src/analyst_questions.py`
|
||||
(never-raise, kill-switch, self-hosting-скоуп — зеркало `coverage_gate`/`serial_gate`); side-effects
|
||||
изолированы в `stage_engine` (`_decide_analysis_outcome` / `_emit_analysis_needs_input` /
|
||||
`_emit_analysis_in_review` / `_emit_analysis_empty`). ORCH-089 autoApprove-блок перенесён
|
||||
**байт-в-байт** (сверено по `git show origin/main`). `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`
|
||||
/ `check_*` / machine-verdict-ключи / схема БД — **подтверждённо не тронуты** (пустой `git diff` по
|
||||
`src/stages.py`, `src/qg/checks.py`, `src/db.py`). Все 11 AC реализованы и покрыты; обязательный
|
||||
регресс-тест TC-01 (Bug-трек, ORCH-019 BR-4) — валидный фиксатор дефекта (красный на дофиксовом
|
||||
`files_ok`-first порядке, зелёный после). Полный регресс `pytest tests/` — **2205 passed** (86s).
|
||||
|
||||
**Блокировавший ранее дефект устранён.** Предыдущая ревизия (v1, run_id=780) выносила
|
||||
`REQUEST_CHANGES` из-за единственного P1 — необновлённой **витрины системы** `docs/overview/`
|
||||
(ось ORCH-011 / ORCH-079). Коммит `19c3177` обновил витрину в этом же PR (см. раздел «Документация»);
|
||||
ось закрыта, `tests/test_system_docs.py` зелёный. Новых P0/P1/P2 нет → **APPROVED**.
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
*(нет)*
|
||||
|
||||
### P1 — Must fix
|
||||
*(нет)*
|
||||
|
||||
### P2 — Should fix
|
||||
*(нет)*
|
||||
|
||||
### P3 — Nice to have
|
||||
- [ ] Косметика (не привязано к правилу, не блокирует): `_decide_analysis_outcome` в gate-off ветке
|
||||
повторно собирает путь `01-questions.md` (`os.path.join` + `os.path.isfile`), который уже
|
||||
инкапсулирован в `analyst_questions.questions_active`; а `_emit_analysis_*` повторно резолвят
|
||||
`get_worktree_path`. Дублирование намеренно (gate-off ветка = «оригинальный байт-в-байт порядок»),
|
||||
поведенчески безвредно — при будущем рефакторе можно консолидировать резолв worktree.
|
||||
|
||||
## Документация
|
||||
|
||||
**Обновлено (проверено по diff) — golden source синхронизирован с кодом:**
|
||||
- `docs/overview/tech-pipeline.md` — абзац «пауза без блокировки» теперь называет **два** источника
|
||||
паузы (оператор `POST /serial-gate/pause` + **движок** авто-park на Needs Input, под флагом
|
||||
`analyst_needs_input_autopause_enabled`, скоуп self-hosting; симметричный unpark на resume).
|
||||
- `docs/overview/tech-observability.md` — пункт `GET /queue` обновлён: пауза/возобновление в serial
|
||||
gate — от оператора **и** от движка (авто-park на Needs Input).
|
||||
- `docs/overview/tech-agents.md` — строка `analyst` дополнена when-applicable сигнальным
|
||||
`01-questions.md` + врезка о канале «блокирующие вопросы → Needs Input».
|
||||
- `docs/architecture/README.md` — новый раздел «Открытые вопросы аналитика → Needs Input (ORCH-120 —
|
||||
реализовано)» со ссылкой на adr-0053.
|
||||
- `CHANGELOG.md` — запись `[Unreleased]` с полным описанием 4 изменений, флагов и витрины.
|
||||
- `docs/_standards/PIPELINE_DOCS.md` — строка манифеста для `01-questions.md` (владелец `analyst`,
|
||||
`when-applicable`, сигнальный, не machine-verdict) + примечание о префиксе `01-` (DQ-4).
|
||||
- `.openclaw/agents/analyst.md` — контракт «блокирующие вопросы → `01-questions.md`, НЕ фабриковать
|
||||
deliverables» + поведение на resume; канон 52d сохранён (анти-дрейф-assert
|
||||
`test_orch120_analyst_documents_questions_channel` + канон-тесты зелёные).
|
||||
- `docs/_templates/01-questions.md` — новый скелет (frontmatter 52c с плейсхолдерами; контекст /
|
||||
блокирующие вопросы / что разблокирует анализ).
|
||||
- `.env.example` — 3 ключа `ORCH_ANALYST_*` с описанием и безопасными дефолтами.
|
||||
- ADR: `docs/work-items/ORCH-120/06-adr/ADR-001-analyst-open-questions-needs-input.md` + сквозной
|
||||
`docs/architecture/adr/adr-0053-analyst-open-questions-needs-input-flow.md`.
|
||||
|
||||
**Обзорные доки / витрина (ORCH-011 / ORCH-079):** `README.md` «Известные ограничения» проверен —
|
||||
**нет** пункта, который закрывается этой задачей (мёртвый путь вопросов не значился ограничением),
|
||||
обновление не требуется. Витрина `docs/overview/` обновлена в том же PR (см. выше). Ось закрыта.
|
||||
|
||||
**Нужно обновить:** ничего.
|
||||
|
||||
## Проверки осей (для прозрачности)
|
||||
|
||||
- **Соответствие ТЗ/AC:** AC-1…AC-11 реализованы и покрыты
|
||||
(`tests/test_orch120_analyst_needs_input.py` TC-01…TC-10, `..._serial_gate_needs_input.py` TC-04
|
||||
интеграционно через реальный `claim_next_job`, `..._resume_unpark.py` TC-05 + autopause-off,
|
||||
`..._questions_artifact_canon.py`, assert канона). Полный регресс **2205 passed**. TC-01 — валидный
|
||||
обязательный фиксатор (RED→GREEN), требование ORCH-019 BR-4 для Bug→escalate full-cycle выполнено.
|
||||
- **Соответствие ADR / трассировка (ORCH-078):** реализация = ADR-001 / adr-0053 (D1…D5, DQ-1…DQ-4).
|
||||
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict / схема БД — байт-в-байт (пустой
|
||||
diff). ОRCH-089 autoApprove перенесён вербатим; ось «пауза» ORCH-124 переиспользована (новых
|
||||
колонок нет); инварианты ORCH-066 (Needs Input только у аналитика — не расширен), ORCH-088/124
|
||||
(serial-gate/пауза — лишь корректно триггерится, код не тронут), ORCH-090 (relaunch-guard — unpark
|
||||
врезан ПОСЛЕ гейта, под `current_stage=='analysis'`, не ослаблен) — сверены, не сломаны.
|
||||
- **Качество кода:** leaf-паттерн чистый (импорт только `os`/`logging`/`config` + ленивый
|
||||
`qg.checks`), never-raise во всех публичных функциях и врезках, kill-switch + скоуп корректны
|
||||
(`questions_gate_applies`/`autopause_applies`), docstrings на публичных функциях содержательны.
|
||||
Freshness-supersede (DQ-2) детерминирован и offline; fail-направление к Needs Input — безопасно
|
||||
(«не строить на домыслах»). Гейт-off ветка восстанавливает оригинальный порядок байт-в-байт (AC-9).
|
||||
- **Документация:** golden source (номерные/стандартные доки, ADR, CHANGELOG, `.env.example`) **и**
|
||||
обзорная витрина `docs/overview/` обновлены в том же PR. Ось обзорных доков закрыта.
|
||||
</content>
|
||||
</invoke>
|
||||
40
docs/work-items/ORCH-120/13-test-report.md
Normal file
40
docs/work-items/ORCH-120/13-test-report.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
result: PASS
|
||||
work_item: ORCH-120
|
||||
stage: testing
|
||||
author_agent: test-runner
|
||||
status: success
|
||||
created_at: 2026-06-17
|
||||
model_used: n/a
|
||||
exit_code: 0
|
||||
smoke: ok
|
||||
---
|
||||
|
||||
# Test Gate Log (deterministic runner, ORCH-116)
|
||||
|
||||
pytest exit-code `0` -> `result: PASS` (smoke: ok).
|
||||
|
||||
Вердикт зафиксирован детерминированным test-раннером (ORCH-116), не LLM. PASS/FAIL = exit-код `pytest` + read-only smoke (`/health`, `/status`, `/queue` + блок `serial_gate`).
|
||||
|
||||
pytest stdout (tail):
|
||||
```
|
||||
............................................. [ 65%]
|
||||
........................................................................ [ 68%]
|
||||
........................................................................ [ 71%]
|
||||
........................................................................ [ 75%]
|
||||
........................................................................ [ 78%]
|
||||
........................................................................ [ 81%]
|
||||
........................................................................ [ 84%]
|
||||
........................................................................ [ 88%]
|
||||
........................................................................ [ 91%]
|
||||
........................................................................ [ 94%]
|
||||
........................................................................ [ 97%]
|
||||
............................................. [100%]
|
||||
=============================== warnings summary ===============================
|
||||
src/config.py:8
|
||||
/repos/_wt/orchestrator/feature_ORCH-120-bug-analyst-open-questions-mus/src/config.py:8: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.13/migration/
|
||||
class Settings(BaseSettings):
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
2205 passed, 1 warning in 102.38s (0:01:42)
|
||||
```
|
||||
12
docs/work-items/ORCH-120/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-120/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-120
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
46
docs/work-items/ORCH-120/15-staging-log.md
Normal file
46
docs/work-items/ORCH-120/15-staging-log.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
work_item: ORCH-120
|
||||
stage: deploy-staging
|
||||
author_agent: staging-runner
|
||||
status: success
|
||||
created_at: 2026-06-17
|
||||
model_used: n/a
|
||||
exit_code: 0
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log (deterministic runner, ORCH-115)
|
||||
|
||||
Staging suite exit-code `0` -> `staging_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным staging-раннером (ORCH-115), не LLM. infra-tolerance (ORCH-061) уже учтена внутри `staging_check.py` — раннер её не пересуживает.
|
||||
|
||||
INFRA-WAIVED lines (ORCH-061, copied for observability):
|
||||
- [33m[1mINFRA-WAIVED:[0m C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
|
||||
Staging suite stdout (tail):
|
||||
```
|
||||
(waiting for analyst job in queue)
|
||||
[33m·[0m waiting... (waiting for analyst job in queue)
|
||||
[33m·[0m waiting... (waiting for analyst job in queue)
|
||||
[33m·[0m waiting... (waiting for analyst job in queue)
|
||||
[33m·[0m waiting... (waiting for analyst job in queue)
|
||||
[33m·[0m waiting... (waiting for analyst job in queue)
|
||||
[31m✗ FAIL[0m C9b Analyst job enqueued in staging queue
|
||||
|
||||
[1m[CLEANUP][0m
|
||||
[33m·[0m CLEANUP: no branch to delete
|
||||
[32m✓ PASS[0m CLEANUP: deleted Plane issue 5db228da-d4be-4001-8233-e1579c6a7074 (HTTP 204)
|
||||
[33m·[0m CLEANUP DB: no task row found for plane_id=5db228da-d4be-4001-8233-e1579c6a7074
|
||||
[33m·[0m CLEANUP DB dedup: no such table: events_dedup
|
||||
|
||||
[1m============================================================[0m
|
||||
[31m[1m RESULT: 8/10 checks PASS[0m
|
||||
REAL failed : none
|
||||
SANDBOX_INFRA failed: ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue']
|
||||
[1m============================================================[0m
|
||||
[33m·[0m tolerance: staging_infra_tolerance_enabled=True
|
||||
[33m[1mINFRA-WAIVED:[0m C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
[1mVERDICT:[0m SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
|
||||
```
|
||||
169
src/analyst_questions.py
Normal file
169
src/analyst_questions.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""ORCH-120 (adr-0053): analyst open-questions -> Needs Input — pure leaf helpers.
|
||||
|
||||
Activates and completes the dead "analyst asks BLOCKING questions ->
|
||||
``01-questions.md`` -> Needs Input" path in
|
||||
``stage_engine._handle_analysis_approved_flow``. This module holds ONLY the pure,
|
||||
unit-testable decision logic; the side effects (set_issue_needs_input / Plane
|
||||
comment / Telegram / auto-park) stay in ``stage_engine``.
|
||||
|
||||
Leaf pattern (mirror of ``coverage_gate`` / ``serial_gate`` / ``labels``): imports
|
||||
only ``os`` / ``logging`` / ``config`` and lazily ``qg.checks.is_self_hosting_repo``;
|
||||
NEVER imports ``stage_engine`` / ``launcher`` / ``db``.
|
||||
|
||||
What it decides (ADR-001 D2/D3):
|
||||
* ``questions_gate_applies(repo)`` — whether the ORCH-120 priority+supersede
|
||||
behaviour is REAL for this repo (kill-switch + scope, mirror of
|
||||
``coverage_gate_applies``). OFF / out-of-scope -> ``stage_engine`` runs its
|
||||
ORIGINAL byte-for-byte order (AC-9).
|
||||
* ``autopause_applies(repo)`` — whether the engine auto-parks a task on Needs
|
||||
Input (and unparks on resume). Independent sub-tumbler AND the questions gate
|
||||
(a task is only ever auto-parked from within the questions-active branch).
|
||||
* ``questions_active(worktree, work_item_id, files_ok)`` — the pure freshness-gated
|
||||
supersede predicate (DQ-2): are there ACTIVE blocking questions that must win
|
||||
over "files ready"?
|
||||
|
||||
never-raise contract (self-hosting safety): every public function degrades
|
||||
conservatively and NEVER propagates into the stage engine / launcher / webhook.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .config import settings
|
||||
|
||||
logger = logging.getLogger("orchestrator.analyst_questions")
|
||||
|
||||
# The analyst's signal artifact (DQ-4: path kept as-is; the engine already reads
|
||||
# exactly this file — see stage_engine._handle_analysis_approved_flow).
|
||||
QUESTIONS_FILENAME = "01-questions.md"
|
||||
|
||||
# The 4 mandatory analysis deliverables that ``check_analysis_complete`` gates on.
|
||||
# Used by the mtime freshness-supersede check (DQ-2): a full FRESH package
|
||||
# supersedes a stale, untouched 01-questions.md left over from a prior run.
|
||||
DELIVERABLES = (
|
||||
"01-brd.md",
|
||||
"02-trz.md",
|
||||
"03-acceptance-criteria.md",
|
||||
"04-test-plan.yaml",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Conditionality (mirrors coverage_gate_applies / serial_gate_applies)
|
||||
# ---------------------------------------------------------------------------
|
||||
def questions_gate_applies(repo: str) -> bool:
|
||||
"""Whether the ORCH-120 questions priority+supersede is REAL for this repo.
|
||||
|
||||
Mirrors the ORCH-22 / ORCH-27 / ORCH-43 pattern:
|
||||
* ``analyst_questions_gate_enabled=False`` -> always False (kill-switch; the
|
||||
engine runs its ORIGINAL pre-ORCH-120 branch order — zero regression, AC-9).
|
||||
* ``analyst_questions_gate_repos`` (CSV) non-empty -> real only for the listed
|
||||
repos.
|
||||
* empty CSV -> real ONLY for the self-hosting repo (``orchestrator``).
|
||||
Never raises (AC-10): any error -> False (the safe no-op default that matches
|
||||
the kill-switch-off behaviour).
|
||||
"""
|
||||
try:
|
||||
if not getattr(settings, "analyst_questions_gate_enabled", False):
|
||||
return False
|
||||
raw = (getattr(settings, "analyst_questions_gate_repos", "") or "").strip()
|
||||
if raw:
|
||||
allowed = {r.strip().lower() for r in raw.split(",") if r.strip()}
|
||||
return (repo or "").strip().lower() in allowed
|
||||
# Lazy import keeps this module a leaf (no qg import at module load).
|
||||
from .qg.checks import is_self_hosting_repo
|
||||
return is_self_hosting_repo(repo)
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.warning("questions_gate_applies error for %s: %s", repo, e)
|
||||
return False
|
||||
|
||||
|
||||
def autopause_applies(repo: str) -> bool:
|
||||
"""Whether the engine auto-parks on Needs Input / unparks on resume (D4/D5).
|
||||
|
||||
Two independent conditions, BOTH required:
|
||||
* ``analyst_needs_input_autopause_enabled`` (independent sub-tumbler; False ->
|
||||
operator-park only, via ``POST /serial-gate/pause``), AND
|
||||
* ``questions_gate_applies(repo)`` — a task is only ever auto-parked from
|
||||
within the questions-active branch, so the auto-park scope can never exceed
|
||||
the questions gate (keeps the off/out-of-scope path byte-for-byte, AC-9).
|
||||
Never raises (AC-10): any error -> False (degrade to operator-park).
|
||||
"""
|
||||
try:
|
||||
if not getattr(settings, "analyst_needs_input_autopause_enabled", False):
|
||||
return False
|
||||
return questions_gate_applies(repo)
|
||||
except Exception as e: # noqa: BLE001 - never-raise contract
|
||||
logger.warning("autopause_applies error for %s: %s", repo, e)
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure freshness-gated supersede predicate (DQ-2)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _work_item_dir(worktree_path: str, work_item_id: str) -> str:
|
||||
return os.path.join(worktree_path, "docs", "work-items", work_item_id)
|
||||
|
||||
|
||||
def questions_active(worktree_path: str, work_item_id: str, files_ok: bool) -> bool:
|
||||
"""Are there ACTIVE blocking questions that must win over "files ready" (DQ-2)?
|
||||
|
||||
Deterministic and OFFLINE (filesystem only — no network, no git):
|
||||
|
||||
* ``01-questions.md`` absent -> NOT active (``False``).
|
||||
* package incomplete (``files_ok is False``) and the file is present -> active
|
||||
(``True``): questions exist, deliverables do not -> questions win (AC-2).
|
||||
* package complete (``files_ok is True``) and the file is present -> freshness
|
||||
check: **superseded iff ALL 4 deliverables are strictly newer** than
|
||||
``01-questions.md`` (by ``os.path.getmtime``). Superseded -> NOT active
|
||||
(``False`` -> In Review, AC-6); otherwise -> active (``True`` -> Needs Input,
|
||||
AC-1). A full FRESH analyst run always writes the 4 deliverables with a newer
|
||||
mtime, so a stale untouched 01-questions.md is deterministically superseded
|
||||
without depending on any LLM action.
|
||||
|
||||
Fail directions (never-raise, AC-10 / DQ-2):
|
||||
* a ``getmtime``/comparison error while the file PROVABLY exists -> treat
|
||||
questions as **active** (``True``, Needs Input) — safe for "don't build on
|
||||
guesses".
|
||||
* a catastrophic error (cannot even determine file presence) -> ``False`` so
|
||||
``stage_engine`` degrades to its prior ``files_ok`` order + WARNING.
|
||||
"""
|
||||
try:
|
||||
questions_path = os.path.join(
|
||||
_work_item_dir(worktree_path, work_item_id), QUESTIONS_FILENAME
|
||||
)
|
||||
present = os.path.isfile(questions_path)
|
||||
except Exception as e: # noqa: BLE001 - catastrophic: cannot determine presence
|
||||
logger.warning(
|
||||
"questions_active: cannot determine 01-questions.md presence for %s: %s",
|
||||
work_item_id, e,
|
||||
)
|
||||
return False
|
||||
|
||||
if not present:
|
||||
return False
|
||||
if not files_ok:
|
||||
# Questions present, deliverables incomplete -> questions take priority.
|
||||
return True
|
||||
|
||||
# Package complete: superseded iff every deliverable is strictly newer than
|
||||
# the questions file. Any mtime error on a proven-existing file -> active.
|
||||
try:
|
||||
q_mtime = os.path.getmtime(questions_path)
|
||||
base = _work_item_dir(worktree_path, work_item_id)
|
||||
for name in DELIVERABLES:
|
||||
dp = os.path.join(base, name)
|
||||
if not os.path.isfile(dp) or not (os.path.getmtime(dp) > q_mtime):
|
||||
# A deliverable is missing or not strictly newer -> NOT superseded
|
||||
# -> questions still active (Needs Input). (files_ok True means the
|
||||
# gate saw all 4; a missing file here is defensive only.)
|
||||
return True
|
||||
# All 4 deliverables strictly newer -> superseded -> In Review.
|
||||
return False
|
||||
except Exception as e: # noqa: BLE001 - mtime error on existing file -> active
|
||||
logger.warning(
|
||||
"questions_active: freshness check failed for %s -> active (Needs Input): %s",
|
||||
work_item_id, e,
|
||||
)
|
||||
return True
|
||||
@@ -1029,6 +1029,40 @@ class Settings(BaseSettings):
|
||||
serial_gate_freeze_enabled: bool = True
|
||||
serial_gate_pause_enabled: bool = True
|
||||
|
||||
# ORCH-120 (adr-0053): analyst open-questions -> Needs Input. Activates and
|
||||
# completes the dead "analyst asks BLOCKING questions -> 01-questions.md ->
|
||||
# Needs Input" path in _handle_analysis_approved_flow. Additive, never-raise,
|
||||
# self-hosting scope; STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict
|
||||
# keys / DB schema are byte-for-byte UNCHANGED (the flow is a pre-gate engine
|
||||
# branch, NOT a Quality Gate; 01-questions.md is a SIGNAL artifact, NOT a
|
||||
# machine-verdict). See docs/work-items/ORCH-120/06-adr/ADR-001-analyst-open-
|
||||
# questions-needs-input.md.
|
||||
# analyst_questions_gate_enabled -> kill-switch (env
|
||||
# ORCH_ANALYST_QUESTIONS_GATE_ENABLED) of the
|
||||
# priority+supersede behaviour (D3). False ->
|
||||
# _handle_analysis_approved_flow runs its ORIGINAL
|
||||
# pre-ORCH-120 order (files_ok first, then a flat
|
||||
# isfile(01-questions.md) check) byte-for-byte (AC-9).
|
||||
# analyst_questions_gate_repos -> CSV scope (env
|
||||
# ORCH_ANALYST_QUESTIONS_GATE_REPOS). Empty -> real
|
||||
# ONLY for the self-hosting repo (orchestrator) via
|
||||
# is_self_hosting_repo; non-empty -> membership.
|
||||
# Mirrors coverage_gate_repos -> enduro untouched.
|
||||
# analyst_needs_input_autopause_enabled -> independent sub-tumbler (env
|
||||
# ORCH_ANALYST_NEEDS_INPUT_AUTOPAUSE_ENABLED) for
|
||||
# auto-park on Needs Input / unpark on resume (D4/D5)
|
||||
# via the ORCH-124 pause axis (db.set_task_paused /
|
||||
# clear_task_paused). True -> a Needs-Input task is
|
||||
# excluded from the serial-gate "active task"
|
||||
# predicate so the repo FIFO does not wedge while we
|
||||
# wait for a human. False -> operator-park only
|
||||
# (POST /serial-gate/pause). Subordinate to the
|
||||
# questions gate (auto-park only fires from the
|
||||
# questions-active branch).
|
||||
analyst_questions_gate_enabled: bool = True
|
||||
analyst_questions_gate_repos: str = ""
|
||||
analyst_needs_input_autopause_enabled: bool = True
|
||||
|
||||
# ORCH-090: STOP-status task cancellation (stop active agent + full progress
|
||||
# reset) and the relaunch-hole close. A new logical Plane key `stop` (fail-closed,
|
||||
# absent from _DEFAULT_STATES) routes to a cancel handler that drives the task to
|
||||
|
||||
@@ -30,7 +30,7 @@ import os
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .db import get_db, update_task_stage, enqueue_job, get_task_track
|
||||
from .db import get_db, update_task_stage, enqueue_job, get_task_track, set_task_paused
|
||||
from .stages import get_next_stage, get_qg_for_stage, get_agent_for_stage
|
||||
from .git_worktree import get_worktree_path
|
||||
from .review_parse import extract_review_findings, extract_test_failures
|
||||
@@ -42,6 +42,7 @@ from . import post_deploy
|
||||
from . import labels
|
||||
from . import bug_fast_track
|
||||
from . import transition_lease
|
||||
from . import analyst_questions
|
||||
from .notifications import (
|
||||
notify_stage_change,
|
||||
notify_qg_failure,
|
||||
@@ -708,84 +709,195 @@ def _handle_analysis_approved_flow(
|
||||
return
|
||||
|
||||
files_ok, _ = files_check(repo, work_item_id, branch)
|
||||
if files_ok:
|
||||
# Full artifacts ready -> In Review, ask for the Approved STATUS (BUG C).
|
||||
set_issue_in_review(work_item_id)
|
||||
plane_add_comment(
|
||||
work_item_id,
|
||||
# task_id is threaded through so build_status_comment can resolve the
|
||||
# analyst duration via agent_runs (ORCH-016 AC-14 DB fallback).
|
||||
_build_analyst_ready_comment(repo, work_item_id, branch, task_id=task_id),
|
||||
author="analyst",
|
||||
)
|
||||
notify_approve_requested(task_id)
|
||||
result.note = "analysis-in-review"
|
||||
logger.info(
|
||||
f"Task {task_id}: analyst finished, requested Approved status in Plane"
|
||||
)
|
||||
|
||||
# --- ORCH-089 autoApprove: auto-pass the BRD human gate by label --------
|
||||
# After In Review + the analyst comment + the approve-request (kept for the
|
||||
# BRD-review clock, transparency and symmetry with the manual path), if the
|
||||
# issue carries the autoApprove label AND the repo is in scope, auto-advance
|
||||
# via the SAME path a human Approved takes — never duplicating the
|
||||
# transition logic. applies() (local, network-free) is checked FIRST so a
|
||||
# disabled kill-switch / out-of-scope repo costs zero network (AC-8); any
|
||||
# error / no-label -> fall through to the prior behaviour (return, wait for
|
||||
# a human, AC-4/AC-6).
|
||||
if labels.auto_approve_applies(repo) and labels.has_label(
|
||||
work_item_id, settings.auto_approve_label
|
||||
):
|
||||
set_issue_approved(work_item_id) # indication (AC-1), transient
|
||||
logger.info(
|
||||
f"Task {task_id}: label {settings.auto_approve_label} -> "
|
||||
f"BRD auto-approved (analysis -> architecture)"
|
||||
)
|
||||
plane_add_comment(
|
||||
work_item_id,
|
||||
f"✅ BRD авто-подтверждён (лейбл {settings.auto_approve_label}). "
|
||||
"Переход на architecture без ручного Approved.",
|
||||
author="analyst",
|
||||
)
|
||||
send_telegram(
|
||||
f"✅ {link_for(work_item_id)}: BRD авто-подтверждён "
|
||||
f"(лейбл {settings.auto_approve_label})."
|
||||
)
|
||||
# Same advance the human Approved webhook uses: finished_agent=None ->
|
||||
# check_analysis_approved approved-via-status -> advance analysis ->
|
||||
# architecture + mark_brd_review_ended (clock) + standard post-effects.
|
||||
# Re-entrancy is safe: the nested call passes finished_agent=None, so it
|
||||
# does NOT re-enter this analyst branch (which requires agent=='analyst').
|
||||
auto = advance_stage(
|
||||
task_id, current_stage, repo, work_item_id, branch, finished_agent=None
|
||||
)
|
||||
result.advanced = auto.advanced
|
||||
result.to_stage = auto.to_stage
|
||||
result.enqueued_agent = auto.enqueued_agent
|
||||
result.enqueued_job_id = auto.enqueued_job_id
|
||||
result.note = "auto-approved-via-label"
|
||||
# ORCH-120 (adr-0053 D3): decide the analyst outcome — 'needs-input' |
|
||||
# 'in-review' | 'empty'. When the questions-gate applies, ACTIVE blocking
|
||||
# questions (01-questions.md, freshness-supersede aware, D2) take PRIORITY over
|
||||
# "files ready" (a fabricated full package must not bury the analyst's blocking
|
||||
# questions, AC-1). Under the kill-switch off / out-of-scope repo the ORIGINAL
|
||||
# pre-ORCH-120 order runs byte-for-byte (files_ok first, then a flat
|
||||
# isfile(01-questions.md) check) — zero regression (AC-9). never-raise (AC-10):
|
||||
# on any error the decision degrades to that original order.
|
||||
outcome = _decide_analysis_outcome(repo, work_item_id, branch, files_ok)
|
||||
if outcome == "needs-input":
|
||||
_emit_analysis_needs_input(task_id, repo, work_item_id, branch, result)
|
||||
return
|
||||
if outcome == "in-review":
|
||||
_emit_analysis_in_review(
|
||||
task_id, current_stage, repo, work_item_id, branch, result
|
||||
)
|
||||
return
|
||||
_emit_analysis_empty(work_item_id, result)
|
||||
|
||||
questions_path = os.path.join(
|
||||
get_worktree_path(repo, branch),
|
||||
f"docs/work-items/{work_item_id}/01-questions.md",
|
||||
|
||||
def _decide_analysis_outcome(repo, work_item_id, branch, files_ok) -> str:
|
||||
"""ORCH-120 (adr-0053 D3): 3-way decision for the analyst outcome.
|
||||
|
||||
Returns ``'needs-input'`` | ``'in-review'`` | ``'empty'``. never-raise (AC-10):
|
||||
on any error degrade to the ORIGINAL pre-ORCH-120 order so behaviour is
|
||||
byte-for-byte the same when the gate is off / out of scope.
|
||||
"""
|
||||
try:
|
||||
worktree = get_worktree_path(repo, branch)
|
||||
except Exception: # noqa: BLE001 - never-raise; treat as no worktree
|
||||
worktree = ""
|
||||
|
||||
# Questions-gate ON: blocking questions take priority over files_ok (D3).
|
||||
try:
|
||||
gate_on = analyst_questions.questions_gate_applies(repo)
|
||||
except Exception: # noqa: BLE001
|
||||
gate_on = False
|
||||
if gate_on:
|
||||
active = None # None == predicate could not decide -> degrade below
|
||||
try:
|
||||
active = analyst_questions.questions_active(
|
||||
worktree, work_item_id, files_ok
|
||||
)
|
||||
except Exception as e: # noqa: BLE001 - never-raise; degrade to original order
|
||||
logger.warning(
|
||||
f"questions_active failed for {work_item_id} -> "
|
||||
f"degrade to original order: {e}"
|
||||
)
|
||||
active = None
|
||||
if active is True:
|
||||
return "needs-input"
|
||||
if active is False:
|
||||
return "in-review" if files_ok else "empty"
|
||||
# active is None: predicate degraded -> fall through to the ORIGINAL order.
|
||||
|
||||
# Kill-switch off / out-of-scope / degraded: ORIGINAL byte-for-byte order
|
||||
# (files_ok first, then a flat isfile(01-questions.md) check).
|
||||
if files_ok:
|
||||
return "in-review"
|
||||
try:
|
||||
questions_path = os.path.join(
|
||||
worktree, f"docs/work-items/{work_item_id}/01-questions.md"
|
||||
)
|
||||
if worktree and os.path.isfile(questions_path):
|
||||
return "needs-input"
|
||||
except Exception: # noqa: BLE001 - never-raise
|
||||
pass
|
||||
return "empty"
|
||||
|
||||
|
||||
def _emit_analysis_in_review(
|
||||
task_id, current_stage, repo, work_item_id, branch, result: AdvanceResult
|
||||
):
|
||||
"""Full artifacts ready -> In Review + request the Approved STATUS (BUG C).
|
||||
|
||||
Carries the ORCH-089 autoApprove insertion verbatim. Extracted from
|
||||
``_handle_analysis_approved_flow`` by ORCH-120 (D3) so the questions-priority
|
||||
decision can dispatch here without changing this branch's behaviour.
|
||||
"""
|
||||
# Full artifacts ready -> In Review, ask for the Approved STATUS (BUG C).
|
||||
set_issue_in_review(work_item_id)
|
||||
plane_add_comment(
|
||||
work_item_id,
|
||||
# task_id is threaded through so build_status_comment can resolve the
|
||||
# analyst duration via agent_runs (ORCH-016 AC-14 DB fallback).
|
||||
_build_analyst_ready_comment(repo, work_item_id, branch, task_id=task_id),
|
||||
author="analyst",
|
||||
)
|
||||
if os.path.isfile(questions_path):
|
||||
set_issue_needs_input(work_item_id)
|
||||
with open(questions_path, "r") as qf:
|
||||
questions_text = qf.read()
|
||||
notify_approve_requested(task_id)
|
||||
result.note = "analysis-in-review"
|
||||
logger.info(
|
||||
f"Task {task_id}: analyst finished, requested Approved status in Plane"
|
||||
)
|
||||
|
||||
# --- ORCH-089 autoApprove: auto-pass the BRD human gate by label --------
|
||||
# After In Review + the analyst comment + the approve-request (kept for the
|
||||
# BRD-review clock, transparency and symmetry with the manual path), if the
|
||||
# issue carries the autoApprove label AND the repo is in scope, auto-advance
|
||||
# via the SAME path a human Approved takes — never duplicating the
|
||||
# transition logic. applies() (local, network-free) is checked FIRST so a
|
||||
# disabled kill-switch / out-of-scope repo costs zero network (AC-8); any
|
||||
# error / no-label -> fall through to the prior behaviour (return, wait for
|
||||
# a human, AC-4/AC-6).
|
||||
if labels.auto_approve_applies(repo) and labels.has_label(
|
||||
work_item_id, settings.auto_approve_label
|
||||
):
|
||||
set_issue_approved(work_item_id) # indication (AC-1), transient
|
||||
logger.info(
|
||||
f"Task {task_id}: label {settings.auto_approve_label} -> "
|
||||
f"BRD auto-approved (analysis -> architecture)"
|
||||
)
|
||||
plane_add_comment(
|
||||
work_item_id,
|
||||
f"\u2753 Analyst \u043d\u0443\u0436\u0434\u0430\u0435\u0442\u0441\u044f \u0432 \u0443\u0442\u043e\u0447\u043d\u0435\u043d\u0438\u0438:\n\n{questions_text}",
|
||||
f"✅ BRD авто-подтверждён (лейбл {settings.auto_approve_label}). "
|
||||
"Переход на architecture без ручного Approved.",
|
||||
author="analyst",
|
||||
)
|
||||
send_telegram(
|
||||
f"\u2753 {link_for(work_item_id)}: Analyst \u0437\u0430\u0434\u0430\u0451\u0442 \u0432\u043e\u043f\u0440\u043e\u0441\u044b. \u041e\u0442\u0432\u0435\u0442\u044c \u0432 Plane."
|
||||
f"✅ {link_for(work_item_id)}: BRD авто-подтверждён "
|
||||
f"(лейбл {settings.auto_approve_label})."
|
||||
)
|
||||
result.note = "analysis-needs-input"
|
||||
return
|
||||
# Same advance the human Approved webhook uses: finished_agent=None ->
|
||||
# check_analysis_approved approved-via-status -> advance analysis ->
|
||||
# architecture + mark_brd_review_ended (clock) + standard post-effects.
|
||||
# Re-entrancy is safe: the nested call passes finished_agent=None, so it
|
||||
# does NOT re-enter this analyst branch (which requires agent=='analyst').
|
||||
auto = advance_stage(
|
||||
task_id, current_stage, repo, work_item_id, branch, finished_agent=None
|
||||
)
|
||||
result.advanced = auto.advanced
|
||||
result.to_stage = auto.to_stage
|
||||
result.enqueued_agent = auto.enqueued_agent
|
||||
result.enqueued_job_id = auto.enqueued_job_id
|
||||
result.note = "auto-approved-via-label"
|
||||
|
||||
# No artifacts and no questions.
|
||||
|
||||
def _emit_analysis_needs_input(
|
||||
task_id, repo, work_item_id, branch, result: AdvanceResult
|
||||
):
|
||||
"""Blocking questions -> Needs Input + Plane comment + Telegram + auto-park.
|
||||
|
||||
The Plane comment / Telegram are preserved verbatim from the original
|
||||
``_handle_analysis_approved_flow`` questions branch. ORCH-120 (D4) adds the
|
||||
auto-park: when ``autopause_applies(repo)`` the task is parked
|
||||
(``db.set_task_paused``) so the repo's serial-gate FIFO is not wedged while we
|
||||
wait for a human (BR-3 / AC-4). never-raise (AC-10): a file-read or park error
|
||||
degrades safely and never crashes ``advance_stage``.
|
||||
"""
|
||||
set_issue_needs_input(work_item_id)
|
||||
questions_text = ""
|
||||
try:
|
||||
questions_path = os.path.join(
|
||||
get_worktree_path(repo, branch),
|
||||
f"docs/work-items/{work_item_id}/01-questions.md",
|
||||
)
|
||||
with open(questions_path, "r") as qf:
|
||||
questions_text = qf.read()
|
||||
except Exception as e: # noqa: BLE001 - never-raise; comment without body
|
||||
logger.warning(
|
||||
f"Task {task_id}: could not read 01-questions.md for {work_item_id}: {e}"
|
||||
)
|
||||
plane_add_comment(
|
||||
work_item_id,
|
||||
f"❓ Analyst нуждается в уточнении:\n\n{questions_text}",
|
||||
author="analyst",
|
||||
)
|
||||
send_telegram(
|
||||
f"❓ {link_for(work_item_id)}: Analyst задаёт вопросы. Ответь в Plane."
|
||||
)
|
||||
# ORCH-120 (D4): auto-park via the ORCH-124 pause axis so the parked task stops
|
||||
# holding the repo serial-gate FIFO. autopause_applies() is gated by the
|
||||
# questions gate, so a kill-switch-off / out-of-scope repo never parks (AC-9).
|
||||
# set_task_paused is already never-raise (-> False); the extra guard keeps a
|
||||
# surprise from crashing advance_stage (AC-10). Park failure does NOT undo the
|
||||
# Needs Input transition (degrades to operator-park, DQ-1).
|
||||
try:
|
||||
if analyst_questions.autopause_applies(repo) and set_task_paused(task_id):
|
||||
logger.info(
|
||||
f"Task {task_id}: auto-parked on Needs Input "
|
||||
f"(serial-gate FIFO freed for {repo})"
|
||||
)
|
||||
except Exception as e: # noqa: BLE001 - never-raise
|
||||
logger.warning(f"Task {task_id}: auto-park on Needs Input failed: {e}")
|
||||
result.note = "analysis-needs-input"
|
||||
|
||||
|
||||
def _emit_analysis_empty(work_item_id, result: AdvanceResult):
|
||||
"""No artifacts and no questions — a warning comment (verbatim original)."""
|
||||
plane_add_comment(
|
||||
work_item_id,
|
||||
"\u26a0\ufe0f Analyst \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043b\u0441\u044f \u0431\u0435\u0437 \u0430\u0440\u0442\u0435\u0444\u0430\u043a\u0442\u043e\u0432 \u0438 \u0431\u0435\u0437 \u0432\u043e\u043f\u0440\u043e\u0441\u043e\u0432. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043b\u043e\u0433.",
|
||||
|
||||
@@ -355,6 +355,26 @@ async def handle_status_start(data: dict, project_id: str = ""):
|
||||
logger.error(f"Failed to post relaunch-hole comment for {work_item_id}: {e}")
|
||||
return
|
||||
|
||||
# ORCH-120 (adr-0053 D5): resume + unpark. The analyst (the sole Needs-Input
|
||||
# owner, ORCH-066) was auto-parked on Needs Input (D4) so the repo serial-gate
|
||||
# FIFO would not wedge while we waited for the stakeholder. Now that they
|
||||
# answered and returned the issue to a working status, clear the pause so the
|
||||
# re-enqueued analyst-job is claimable (jointly with the ORCH-126 stale
|
||||
# run_id/pid fix). Gated on `analysis` + autopause_applies (questions gate +
|
||||
# scope) so off/out-of-scope is byte-for-byte unchanged (AC-9); idempotent and
|
||||
# never-raise — a resume that was never parked is a no-op.
|
||||
if current_stage == "analysis":
|
||||
try:
|
||||
from .. import analyst_questions
|
||||
from ..db import clear_task_paused
|
||||
if analyst_questions.autopause_applies(repo) and clear_task_paused(task_id):
|
||||
logger.info(
|
||||
f"Task {task_id}: unparked on analyst resume "
|
||||
f"(serial-gate FIFO re-entered for {repo})"
|
||||
)
|
||||
except Exception as e: # noqa: BLE001 - never-raise: resume must not crash
|
||||
logger.warning(f"Task {task_id}: unpark on resume failed: {e}")
|
||||
|
||||
task_desc = (
|
||||
f"Work item: {work_item_id}\nRepo: {repo}\nBranch: {branch}\n"
|
||||
f"Stage: {current_stage}\nNote: Stakeholder returned the issue to In "
|
||||
|
||||
@@ -282,6 +282,27 @@ def test_reviewer_carries_overview_docs_axis():
|
||||
)
|
||||
|
||||
|
||||
def test_orch120_analyst_documents_questions_channel():
|
||||
"""ORCH-120 (adr-0053) TC-07 (AC-7): analyst.md documents the 01-questions.md
|
||||
Needs-Input channel (blocking questions -> Needs Input, do NOT fabricate
|
||||
deliverables) without breaking the 52d canon (guarded by the canon tests above).
|
||||
|
||||
Anchored explicitly so a future prompt refactor cannot silently drop the
|
||||
working contract (the same anti-drift rationale as the traceability axis)."""
|
||||
text = _read("analyst")
|
||||
assert "01-questions.md" in text, (
|
||||
"analyst.md does not document the 01-questions.md Needs-Input channel"
|
||||
)
|
||||
assert "Needs Input" in text, (
|
||||
"analyst.md does not tie 01-questions.md to the Needs Input outcome"
|
||||
)
|
||||
# Must instruct NOT to fabricate deliverables (the core BR-1 contract).
|
||||
assert "фабрик" in text, (
|
||||
"analyst.md does not carry the 'do not fabricate deliverables' instruction"
|
||||
)
|
||||
assert "ORCH-120" in text, "analyst.md does not anchor the questions channel to ORCH-120"
|
||||
|
||||
|
||||
def test_reviewer_overview_axis_covers_system_showcase():
|
||||
"""ORCH-011 (ADR-001 D7): the ORCH-079 overview-docs axis explicitly extends
|
||||
to the system showcase `docs/overview/` — a PR changing functionality described
|
||||
|
||||
305
tests/test_orch120_analyst_needs_input.py
Normal file
305
tests/test_orch120_analyst_needs_input.py
Normal file
@@ -0,0 +1,305 @@
|
||||
"""ORCH-120 (adr-0053): analyst open-questions -> Needs Input — engine flow.
|
||||
|
||||
Drives ``_handle_analysis_approved_flow`` through the real ``advance_stage(...,
|
||||
finished_agent='analyst')`` launcher path (pattern of
|
||||
``tests/test_auto_approve_brd.py``): mocks the Plane/Telegram setters and uses a
|
||||
temporary worktree + a patched ``check_analysis_complete``.
|
||||
|
||||
Covers (04-test-plan.yaml):
|
||||
TC-01 REGRESS (mandatory): 4 files + ACTIVE 01-questions.md simultaneously ->
|
||||
Needs Input wins over "files ready" (AC-1). RED before the fix.
|
||||
TC-02 01-questions.md present, 4 files missing -> Needs Input, question text in
|
||||
the Plane comment + Telegram (AC-2).
|
||||
TC-03 Happy-path: no 01-questions.md, 4 files present -> In Review (AC-3).
|
||||
TC-06 Hygiene: full FRESH package supersedes a stale 01-questions.md -> In
|
||||
Review, NOT a repeat Needs Input (AC-6).
|
||||
TC-09 never-raise: a failure in the new logic degrades safely + does not crash
|
||||
advance_stage (AC-10).
|
||||
TC-10 Reversibility: kill-switch off OR enduro repo -> ORIGINAL byte-for-byte
|
||||
order (files_ok first) (AC-9).
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
_test_db = os.path.join(tempfile.gettempdir(), "test_orch120_needs_input.db")
|
||||
os.environ["ORCH_DB_PATH"] = _test_db
|
||||
os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir()
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
|
||||
from unittest.mock import MagicMock # noqa: E402
|
||||
|
||||
import src.db as _db # noqa: E402
|
||||
from src.db import init_db, get_db # noqa: E402
|
||||
from src import stage_engine # noqa: E402
|
||||
from src import analyst_questions # noqa: E402
|
||||
from src import labels # noqa: E402
|
||||
from src.config import settings # noqa: E402
|
||||
from src.stage_engine import advance_stage # noqa: E402
|
||||
|
||||
_DELIVERABLES = ("01-brd.md", "02-trz.md", "03-acceptance-criteria.md", "04-test-plan.yaml")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fresh(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(_db.settings, "db_path", _test_db)
|
||||
if os.path.exists(_test_db):
|
||||
os.unlink(_test_db)
|
||||
init_db()
|
||||
# Silence Plane/Telegram side effects; capture the channels we assert on.
|
||||
for name in ("notify_stage_change", "notify_qg_failure", "plane_notify_stage",
|
||||
"plane_notify_qg", "set_issue_in_review", "set_issue_needs_input",
|
||||
"set_issue_approved", "notify_approve_requested"):
|
||||
monkeypatch.setattr(stage_engine, name, MagicMock(), raising=False)
|
||||
monkeypatch.setattr(stage_engine, "_build_analyst_ready_comment",
|
||||
lambda *a, **k: "ready", raising=False)
|
||||
monkeypatch.setattr(stage_engine, "plane_add_comment", MagicMock())
|
||||
monkeypatch.setattr(stage_engine, "send_telegram", MagicMock())
|
||||
# autoApprove off by default (TC-03 wants In Review, not auto-advance).
|
||||
monkeypatch.setattr(labels, "auto_approve_applies", lambda repo: False)
|
||||
monkeypatch.setattr(labels, "has_label", lambda w, name, p=None: False)
|
||||
# Questions-gate on for orchestrator by default (mirror prod defaults).
|
||||
monkeypatch.setattr(settings, "analyst_questions_gate_enabled", True, raising=False)
|
||||
monkeypatch.setattr(settings, "analyst_questions_gate_repos", "", raising=False)
|
||||
monkeypatch.setattr(settings, "analyst_needs_input_autopause_enabled", True,
|
||||
raising=False)
|
||||
yield
|
||||
|
||||
|
||||
def _make_task(stage="analysis", repo="orchestrator", branch="feature/ORCH-120-x",
|
||||
wi="ORCH-120"):
|
||||
conn = get_db()
|
||||
cur = conn.execute(
|
||||
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(f"plane-{wi}", wi, repo, branch, stage),
|
||||
)
|
||||
tid = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return tid
|
||||
|
||||
|
||||
def _wi_dir(worktree, wi="ORCH-120"):
|
||||
d = os.path.join(worktree, "docs", "work-items", wi)
|
||||
os.makedirs(d, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
def _write(path, mtime=None, body="x"):
|
||||
with open(path, "w") as f:
|
||||
f.write(body)
|
||||
if mtime is not None:
|
||||
os.utime(path, (mtime, mtime))
|
||||
|
||||
|
||||
def _patch_worktree(monkeypatch, worktree):
|
||||
monkeypatch.setattr(stage_engine, "get_worktree_path", lambda repo, branch: worktree)
|
||||
|
||||
|
||||
def _patch_complete_gate(monkeypatch, ok=True):
|
||||
def gate(*a, **k):
|
||||
return (ok, "ok" if ok else "missing artifacts")
|
||||
monkeypatch.setattr(
|
||||
stage_engine, "QG_CHECKS",
|
||||
{**stage_engine.QG_CHECKS, "check_analysis_complete": gate},
|
||||
)
|
||||
|
||||
|
||||
def _stage_of(task_id):
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT stage FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
conn.close()
|
||||
return row[0]
|
||||
|
||||
|
||||
def _paused_at(task_id):
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT paused_at FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
conn.close()
|
||||
return row[0]
|
||||
|
||||
|
||||
# --- TC-01: REGRESS — questions priority over files_ok -----------------------
|
||||
def test_tc01_questions_priority_over_files_ready(monkeypatch, tmp_path):
|
||||
"""4 deliverables + an ACTIVE (newest) 01-questions.md -> Needs Input wins."""
|
||||
worktree = str(tmp_path)
|
||||
d = _wi_dir(worktree)
|
||||
base = 1_000_000
|
||||
for i, name in enumerate(_DELIVERABLES):
|
||||
_write(os.path.join(d, name), mtime=base + i)
|
||||
# 01-questions.md is the NEWEST -> NOT superseded -> active.
|
||||
_write(os.path.join(d, "01-questions.md"), mtime=base + 100,
|
||||
body="Q-1 нужно уточнить охват")
|
||||
_patch_worktree(monkeypatch, worktree)
|
||||
_patch_complete_gate(monkeypatch, ok=True)
|
||||
tid = _make_task()
|
||||
|
||||
res = advance_stage(tid, "analysis", "orchestrator", "ORCH-120",
|
||||
"feature/ORCH-120-x", finished_agent="analyst")
|
||||
|
||||
assert res.note == "analysis-needs-input"
|
||||
assert stage_engine.set_issue_needs_input.called
|
||||
assert not stage_engine.set_issue_in_review.called
|
||||
assert _stage_of(tid) == "analysis" # NOT advanced to architecture
|
||||
|
||||
|
||||
# --- TC-02: questions only, no deliverables ----------------------------------
|
||||
def test_tc02_questions_only_no_deliverables(monkeypatch, tmp_path):
|
||||
worktree = str(tmp_path)
|
||||
d = _wi_dir(worktree)
|
||||
_write(os.path.join(d, "01-questions.md"), body="Q-1 какой формат вывода?")
|
||||
_patch_worktree(monkeypatch, worktree)
|
||||
_patch_complete_gate(monkeypatch, ok=False)
|
||||
tid = _make_task()
|
||||
|
||||
res = advance_stage(tid, "analysis", "orchestrator", "ORCH-120",
|
||||
"feature/ORCH-120-x", finished_agent="analyst")
|
||||
|
||||
assert res.note == "analysis-needs-input"
|
||||
assert stage_engine.set_issue_needs_input.called
|
||||
# Question text reached the Plane comment + Telegram.
|
||||
comment_arg = stage_engine.plane_add_comment.call_args.args[1]
|
||||
assert "Q-1 какой формат вывода?" in comment_arg
|
||||
assert stage_engine.send_telegram.called
|
||||
|
||||
|
||||
# --- TC-03: happy-path, no questions -----------------------------------------
|
||||
def test_tc03_happy_path_no_questions(monkeypatch, tmp_path):
|
||||
worktree = str(tmp_path)
|
||||
d = _wi_dir(worktree)
|
||||
for name in _DELIVERABLES:
|
||||
_write(os.path.join(d, name))
|
||||
# No 01-questions.md.
|
||||
_patch_worktree(monkeypatch, worktree)
|
||||
_patch_complete_gate(monkeypatch, ok=True)
|
||||
tid = _make_task()
|
||||
|
||||
res = advance_stage(tid, "analysis", "orchestrator", "ORCH-120",
|
||||
"feature/ORCH-120-x", finished_agent="analyst")
|
||||
|
||||
assert res.note == "analysis-in-review"
|
||||
assert stage_engine.set_issue_in_review.called
|
||||
assert not stage_engine.set_issue_needs_input.called
|
||||
assert stage_engine.notify_approve_requested.called
|
||||
|
||||
|
||||
# --- TC-06: hygiene — fresh package supersedes a stale questions file --------
|
||||
def test_tc06_stale_questions_superseded(monkeypatch, tmp_path):
|
||||
worktree = str(tmp_path)
|
||||
d = _wi_dir(worktree)
|
||||
base = 2_000_000
|
||||
# 01-questions.md is OLDER than every deliverable -> superseded -> In Review.
|
||||
_write(os.path.join(d, "01-questions.md"), mtime=base, body="stale Q from last run")
|
||||
for i, name in enumerate(_DELIVERABLES):
|
||||
_write(os.path.join(d, name), mtime=base + 100 + i)
|
||||
_patch_worktree(monkeypatch, worktree)
|
||||
_patch_complete_gate(monkeypatch, ok=True)
|
||||
tid = _make_task()
|
||||
|
||||
res = advance_stage(tid, "analysis", "orchestrator", "ORCH-120",
|
||||
"feature/ORCH-120-x", finished_agent="analyst")
|
||||
|
||||
assert res.note == "analysis-in-review"
|
||||
assert stage_engine.set_issue_in_review.called
|
||||
assert not stage_engine.set_issue_needs_input.called
|
||||
|
||||
|
||||
# --- TC-09: never-raise -------------------------------------------------------
|
||||
def test_tc09_predicate_error_degrades_to_prior_order(monkeypatch, tmp_path):
|
||||
"""questions_active raising -> degrade to original order (files_ok -> In Review)."""
|
||||
worktree = str(tmp_path)
|
||||
_wi_dir(worktree)
|
||||
_patch_worktree(monkeypatch, worktree)
|
||||
_patch_complete_gate(monkeypatch, ok=True)
|
||||
|
||||
def boom(*a, **k):
|
||||
raise RuntimeError("synthetic predicate failure")
|
||||
monkeypatch.setattr(analyst_questions, "questions_active", boom)
|
||||
tid = _make_task()
|
||||
|
||||
# Must NOT raise.
|
||||
res = advance_stage(tid, "analysis", "orchestrator", "ORCH-120",
|
||||
"feature/ORCH-120-x", finished_agent="analyst")
|
||||
assert res.note == "analysis-in-review"
|
||||
assert stage_engine.set_issue_in_review.called
|
||||
|
||||
|
||||
def test_tc09_park_error_does_not_crash(monkeypatch, tmp_path):
|
||||
"""A failing set_task_paused must not undo Needs Input nor crash advance_stage."""
|
||||
worktree = str(tmp_path)
|
||||
d = _wi_dir(worktree)
|
||||
_write(os.path.join(d, "01-questions.md"), body="Q-1 ?")
|
||||
_patch_worktree(monkeypatch, worktree)
|
||||
_patch_complete_gate(monkeypatch, ok=False)
|
||||
|
||||
def boom(task_id):
|
||||
raise RuntimeError("synthetic park failure")
|
||||
monkeypatch.setattr(stage_engine, "set_task_paused", boom)
|
||||
tid = _make_task()
|
||||
|
||||
res = advance_stage(tid, "analysis", "orchestrator", "ORCH-120",
|
||||
"feature/ORCH-120-x", finished_agent="analyst")
|
||||
assert res.note == "analysis-needs-input"
|
||||
assert stage_engine.set_issue_needs_input.called
|
||||
|
||||
|
||||
# --- TC-10: reversibility — kill-switch off / enduro -> original order --------
|
||||
def test_tc10_kill_switch_off_original_order(monkeypatch, tmp_path):
|
||||
"""Gate off: 4 files + active questions -> In Review (original order), no park."""
|
||||
worktree = str(tmp_path)
|
||||
d = _wi_dir(worktree)
|
||||
base = 3_000_000
|
||||
for i, name in enumerate(_DELIVERABLES):
|
||||
_write(os.path.join(d, name), mtime=base + i)
|
||||
_write(os.path.join(d, "01-questions.md"), mtime=base + 100, body="Q-1 ?")
|
||||
_patch_worktree(monkeypatch, worktree)
|
||||
_patch_complete_gate(monkeypatch, ok=True)
|
||||
monkeypatch.setattr(settings, "analyst_questions_gate_enabled", False, raising=False)
|
||||
tid = _make_task()
|
||||
|
||||
res = advance_stage(tid, "analysis", "orchestrator", "ORCH-120",
|
||||
"feature/ORCH-120-x", finished_agent="analyst")
|
||||
|
||||
assert res.note == "analysis-in-review"
|
||||
assert stage_engine.set_issue_in_review.called
|
||||
assert not stage_engine.set_issue_needs_input.called
|
||||
assert _paused_at(tid) is None # no auto-park when the gate is off
|
||||
|
||||
|
||||
def test_tc10_enduro_out_of_scope_original_order(monkeypatch, tmp_path):
|
||||
"""enduro repo (empty CSV -> self-hosting only) -> gate inert -> original order."""
|
||||
worktree = str(tmp_path)
|
||||
d = _wi_dir(worktree, wi="ET-9")
|
||||
base = 4_000_000
|
||||
for i, name in enumerate(_DELIVERABLES):
|
||||
_write(os.path.join(d, name), mtime=base + i)
|
||||
_write(os.path.join(d, "01-questions.md"), mtime=base + 100, body="Q-1 ?")
|
||||
_patch_worktree(monkeypatch, worktree)
|
||||
_patch_complete_gate(monkeypatch, ok=True)
|
||||
tid = _make_task(repo="enduro-trails", branch="feature/ET-9-x", wi="ET-9")
|
||||
|
||||
res = advance_stage(tid, "analysis", "enduro-trails", "ET-9",
|
||||
"feature/ET-9-x", finished_agent="analyst")
|
||||
|
||||
assert res.note == "analysis-in-review"
|
||||
assert stage_engine.set_issue_in_review.called
|
||||
assert not stage_engine.set_issue_needs_input.called
|
||||
|
||||
|
||||
# --- Auto-park bonus: orchestrator Needs Input parks the task ----------------
|
||||
def test_autopark_on_needs_input(monkeypatch, tmp_path):
|
||||
"""ORCH-120 D4: Needs Input on the self-hosting repo auto-parks the task."""
|
||||
worktree = str(tmp_path)
|
||||
d = _wi_dir(worktree)
|
||||
_write(os.path.join(d, "01-questions.md"), body="Q-1 ?")
|
||||
_patch_worktree(monkeypatch, worktree)
|
||||
_patch_complete_gate(monkeypatch, ok=False)
|
||||
tid = _make_task()
|
||||
|
||||
advance_stage(tid, "analysis", "orchestrator", "ORCH-120",
|
||||
"feature/ORCH-120-x", finished_agent="analyst")
|
||||
|
||||
assert _paused_at(tid) is not None # task parked -> serial-gate FIFO freed
|
||||
56
tests/test_orch120_questions_artifact_canon.py
Normal file
56
tests/test_orch120_questions_artifact_canon.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""ORCH-120 (adr-0053) TC-08: canon of the 01-questions.md signal artifact.
|
||||
|
||||
Pure-text structural checks (NO src/ import): the skeleton exists and the
|
||||
PIPELINE_DOCS.md manifest documents 01-questions.md as an analyst-owned,
|
||||
when-applicable, NON-machine-verdict signal artifact (AC-8).
|
||||
"""
|
||||
import os
|
||||
|
||||
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
_TEMPLATE = os.path.join(_REPO_ROOT, "docs", "_templates", "01-questions.md")
|
||||
_PIPELINE_DOCS = os.path.join(_REPO_ROOT, "docs", "_standards", "PIPELINE_DOCS.md")
|
||||
|
||||
|
||||
def _read(path):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def test_questions_skeleton_exists_and_nonempty():
|
||||
"""TC-08: docs/_templates/01-questions.md exists and is non-empty."""
|
||||
assert os.path.isfile(_TEMPLATE), "docs/_templates/01-questions.md is missing"
|
||||
body = _read(_TEMPLATE)
|
||||
assert body.strip(), "01-questions.md template is empty"
|
||||
# Carries the 52c schema fields + signals the analyst stage/owner.
|
||||
for field in ("work_item", "stage", "author_agent", "status", "created_at",
|
||||
"model_used"):
|
||||
assert field in body, f"01-questions.md template omits schema field {field!r}"
|
||||
assert "stage: analysis" in body
|
||||
assert "author_agent: analyst" in body
|
||||
# Documents the three required parts (context / blocking questions / unblocks).
|
||||
assert "Блокирующие вопросы" in body
|
||||
assert "разблокирует" in body
|
||||
|
||||
|
||||
def test_pipeline_docs_manifest_row_for_questions():
|
||||
"""TC-08: PIPELINE_DOCS.md has a manifest row for 01-questions.md."""
|
||||
text = _read(_PIPELINE_DOCS)
|
||||
# A manifest table row mentioning the file, the analyst owner and when-applicable.
|
||||
rows = [ln for ln in text.splitlines()
|
||||
if "`01-questions.md`" in ln and "|" in ln]
|
||||
assert rows, "PIPELINE_DOCS.md has no manifest row for 01-questions.md"
|
||||
row = rows[0]
|
||||
assert "analyst" in row, "01-questions.md manifest row does not name the analyst owner"
|
||||
assert "when-applicable" in row, "01-questions.md row not marked when-applicable"
|
||||
# Signal artifact -> NOT a machine-verdict.
|
||||
assert ("сигнальн" in row.lower()) or ("не machine-verdict" in row.lower()), (
|
||||
"01-questions.md row does not mark it as a signal / non-machine-verdict artifact"
|
||||
)
|
||||
|
||||
|
||||
def test_pipeline_docs_notes_the_01_prefix_convention():
|
||||
"""TC-08 (DQ-4): PIPELINE_DOCS.md notes the shared `01-` prefix convention."""
|
||||
text = _read(_PIPELINE_DOCS)
|
||||
assert "Префикс `01-`" in text or "префикс `01-`" in text, (
|
||||
"PIPELINE_DOCS.md does not document the 01- prefix convention (DQ-4)"
|
||||
)
|
||||
100
tests/test_orch120_resume_unpark.py
Normal file
100
tests/test_orch120_resume_unpark.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""ORCH-120 (adr-0053) TC-05: resume + unpark on analyst relaunch.
|
||||
|
||||
When the stakeholder answers and returns the issue to a working status on
|
||||
``analysis``, ``handle_status_start`` clears the auto-park (``paused_at`` -> NULL)
|
||||
so the re-enqueued analyst-job is claimable, and relaunches the analyst. The
|
||||
ORCH-090 relaunch-guard (relaunch only for ``analysis``) is not weakened.
|
||||
|
||||
TC-05 paused analysis task + To Analyse, no active job -> unpark + relaunch
|
||||
analyst; relaunch-guard (analysis only) respected (AC-5).
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
_test_db = os.path.join(tempfile.gettempdir(), "test_orch120_resume_unpark.db")
|
||||
os.environ["ORCH_DB_PATH"] = _test_db
|
||||
os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir()
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
|
||||
from unittest.mock import patch, AsyncMock, MagicMock # noqa: E402
|
||||
|
||||
import src.db as _db # noqa: E402
|
||||
from src.db import init_db, get_db, set_task_paused, is_task_paused # noqa: E402
|
||||
from src import config as cfg # noqa: E402
|
||||
from src.webhooks.plane import handle_status_start # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fresh(monkeypatch):
|
||||
monkeypatch.setattr(_db.settings, "db_path", _test_db)
|
||||
if os.path.exists(_test_db):
|
||||
os.unlink(_test_db)
|
||||
init_db()
|
||||
# Questions gate + autopause ON for the self-hosting repo (prod defaults).
|
||||
monkeypatch.setattr(cfg.settings, "analyst_questions_gate_enabled", True, raising=False)
|
||||
monkeypatch.setattr(cfg.settings, "analyst_questions_gate_repos", "", raising=False)
|
||||
monkeypatch.setattr(cfg.settings, "analyst_needs_input_autopause_enabled", True,
|
||||
raising=False)
|
||||
yield
|
||||
if os.path.exists(_test_db):
|
||||
os.unlink(_test_db)
|
||||
|
||||
|
||||
def _make_task(plane_id, stage="analysis", repo="orchestrator", branch="feature/ORCH-120-x",
|
||||
wi="ORCH-120"):
|
||||
conn = get_db()
|
||||
cur = conn.execute(
|
||||
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(plane_id, wi, repo, branch, stage),
|
||||
)
|
||||
tid = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return tid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tc05_resume_unparks_and_relaunches_analyst():
|
||||
tid = _make_task("resume-120", stage="analysis")
|
||||
assert set_task_paused(tid) is True
|
||||
assert is_task_paused(tid) is True # parked while waiting for a human
|
||||
|
||||
data = {"id": "resume-120", "state": {"id": "ip-uuid", "name": "To Analyse"}}
|
||||
with patch("src.webhooks.plane.enqueue_job", return_value=11) as mock_enqueue, \
|
||||
patch("src.webhooks.plane.start_pipeline", new_callable=AsyncMock) as mock_start, \
|
||||
patch("src.plane_sync.add_comment", MagicMock()), \
|
||||
patch("src.plane_sync.set_issue_analysis") as mock_analysis:
|
||||
await handle_status_start(data, "proj-1")
|
||||
|
||||
mock_start.assert_not_called() # resume, not a fresh pipeline
|
||||
# Unparked: the re-enqueued analyst-job is claimable again (AC-5).
|
||||
assert is_task_paused(tid) is False
|
||||
# Analyst relaunched exactly once (relaunch-guard: only analysis, ORCH-090).
|
||||
assert mock_enqueue.call_count == 1
|
||||
assert mock_enqueue.call_args.args[0] == "analyst"
|
||||
mock_analysis.assert_called_once_with("ORCH-120")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tc05_autopause_off_does_not_unpark():
|
||||
"""Reversibility: with the autopause sub-flag off the resume does not unpark
|
||||
(operator-park stays the operator's to clear); relaunch still happens."""
|
||||
tid = _make_task("resume-120b", stage="analysis")
|
||||
set_task_paused(tid)
|
||||
|
||||
data = {"id": "resume-120b", "state": {"id": "ip-uuid", "name": "To Analyse"}}
|
||||
with patch("src.config.settings.analyst_needs_input_autopause_enabled", False, create=True), \
|
||||
patch("src.webhooks.plane.enqueue_job", return_value=12) as mock_enqueue, \
|
||||
patch("src.webhooks.plane.start_pipeline", new_callable=AsyncMock), \
|
||||
patch("src.plane_sync.add_comment", MagicMock()), \
|
||||
patch("src.plane_sync.set_issue_analysis"):
|
||||
await handle_status_start(data, "proj-1")
|
||||
|
||||
# Autopause off -> engine-side unpark is inert; the task stays parked (operator
|
||||
# clears via POST /serial-gate/resume). The relaunch itself is unaffected.
|
||||
assert is_task_paused(tid) is True
|
||||
assert mock_enqueue.call_count == 1
|
||||
111
tests/test_orch120_serial_gate_needs_input.py
Normal file
111
tests/test_orch120_serial_gate_needs_input.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""ORCH-120 (adr-0053) TC-04: Needs Input auto-park does not wedge serial-gate.
|
||||
|
||||
Integration: a self-hosting task A driven into Needs Input through the real
|
||||
``advance_stage(..., finished_agent='analyst')`` path is auto-parked (D4), which
|
||||
EXCLUDES it from the serial-gate "active task" predicate (ORCH-088/124) so a later
|
||||
task B of the same repo can enter ``analysis`` (its analyst-job becomes claimable).
|
||||
|
||||
TC-04 earlier task A (analysis) blocks B's analyst-job; once A is driven into
|
||||
Needs Input (auto-parked), B's analyst-job becomes claimable (AC-4).
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
_test_db = os.path.join(tempfile.gettempdir(), "test_orch120_serial_gate.db")
|
||||
os.environ["ORCH_DB_PATH"] = _test_db
|
||||
os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir()
|
||||
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
||||
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
||||
|
||||
from unittest.mock import MagicMock # noqa: E402
|
||||
|
||||
import src.db as _db # noqa: E402
|
||||
from src.db import init_db, get_db, enqueue_job, claim_next_job # noqa: E402
|
||||
from src import stage_engine # noqa: E402
|
||||
from src import labels # noqa: E402
|
||||
from src import config as cfg # noqa: E402
|
||||
from src.stage_engine import advance_stage # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fresh(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(_db.settings, "db_path", _test_db)
|
||||
if os.path.exists(_test_db):
|
||||
os.unlink(_test_db)
|
||||
init_db()
|
||||
# Serial gate + pause axis ON (empty CSV -> all repos).
|
||||
monkeypatch.setattr(cfg.settings, "serial_gate_enabled", True, raising=False)
|
||||
monkeypatch.setattr(cfg.settings, "serial_gate_repos", "", raising=False)
|
||||
monkeypatch.setattr(cfg.settings, "serial_gate_freeze_enabled", True, raising=False)
|
||||
monkeypatch.setattr(cfg.settings, "serial_gate_pause_enabled", True, raising=False)
|
||||
monkeypatch.setattr(cfg.settings, "task_deps_enabled", False, raising=False)
|
||||
# Questions gate + autopause ON for orchestrator (prod defaults).
|
||||
monkeypatch.setattr(cfg.settings, "analyst_questions_gate_enabled", True, raising=False)
|
||||
monkeypatch.setattr(cfg.settings, "analyst_questions_gate_repos", "", raising=False)
|
||||
monkeypatch.setattr(cfg.settings, "analyst_needs_input_autopause_enabled", True,
|
||||
raising=False)
|
||||
# Silence Plane/Telegram side effects.
|
||||
for name in ("set_issue_in_review", "set_issue_needs_input", "set_issue_approved",
|
||||
"notify_approve_requested", "plane_notify_stage"):
|
||||
monkeypatch.setattr(stage_engine, name, MagicMock(), raising=False)
|
||||
monkeypatch.setattr(stage_engine, "plane_add_comment", MagicMock())
|
||||
monkeypatch.setattr(stage_engine, "send_telegram", MagicMock())
|
||||
monkeypatch.setattr(labels, "auto_approve_applies", lambda repo: False)
|
||||
monkeypatch.setattr(labels, "has_label", lambda w, name, p=None: False)
|
||||
yield
|
||||
|
||||
|
||||
def _make_task(wi, stage="analysis", repo="orchestrator"):
|
||||
conn = get_db()
|
||||
cur = conn.execute(
|
||||
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(wi, wi, repo, f"feature/{wi}", stage),
|
||||
)
|
||||
tid = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return tid
|
||||
|
||||
|
||||
def _paused_at(task_id):
|
||||
conn = get_db()
|
||||
row = conn.execute("SELECT paused_at FROM tasks WHERE id=?", (task_id,)).fetchone()
|
||||
conn.close()
|
||||
return row[0]
|
||||
|
||||
|
||||
def test_tc04_needs_input_autopark_unblocks_serial_gate(monkeypatch, tmp_path):
|
||||
a = _make_task("ORCH-120A", stage="analysis") # earlier active task
|
||||
b = _make_task("ORCH-120B", stage="analysis") # later task awaiting analysis
|
||||
job_b = enqueue_job("analyst", "orchestrator", "B", task_id=b)
|
||||
|
||||
# Before A is parked it holds the repo's FIFO gate -> B's analyst-job blocked.
|
||||
assert claim_next_job() is None, "active A gates B (FIFO, ORCH-088)"
|
||||
|
||||
# Drive A into Needs Input via the engine (01-questions.md present, no
|
||||
# deliverables) -> auto-park.
|
||||
worktree = str(tmp_path)
|
||||
d = os.path.join(worktree, "docs", "work-items", "ORCH-120A")
|
||||
os.makedirs(d, exist_ok=True)
|
||||
with open(os.path.join(d, "01-questions.md"), "w") as f:
|
||||
f.write("Q-1 нужно уточнить охват")
|
||||
monkeypatch.setattr(stage_engine, "get_worktree_path", lambda repo, br: worktree)
|
||||
monkeypatch.setattr(
|
||||
stage_engine, "QG_CHECKS",
|
||||
{**stage_engine.QG_CHECKS,
|
||||
"check_analysis_complete": lambda *a, **k: (False, "missing")},
|
||||
)
|
||||
|
||||
res = advance_stage(a, "analysis", "orchestrator", "ORCH-120A",
|
||||
"feature/ORCH-120A", finished_agent="analyst")
|
||||
assert res.note == "analysis-needs-input"
|
||||
assert _paused_at(a) is not None, "A must be auto-parked on Needs Input (D4)"
|
||||
|
||||
# Now the parked A no longer holds the FIFO gate -> B's analyst-job is claimable.
|
||||
claimed = claim_next_job()
|
||||
assert claimed is not None and claimed["id"] == job_b, (
|
||||
"a parked Needs-Input predecessor must not wedge the repo serial-gate (AC-4)"
|
||||
)
|
||||
Reference in New Issue
Block a user