Compare commits
6 Commits
fix/tests-
...
fix/isolat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1baae81165 | ||
|
|
e856e0940b | ||
|
|
7bbab9c38b | ||
| a33a971c9c | |||
|
|
d0c604bc66 | ||
| 83f5020f94 |
22
.gitea/workflows/ci.yml
Normal file
22
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["feature/**", "bugfix/**", "hotfix/**", "fix/**", "ci/**"]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python3 -m pip install --user --upgrade pip
|
||||||
|
python3 -m pip install --user -r requirements.txt
|
||||||
|
- name: Test
|
||||||
|
env:
|
||||||
|
PYTHONPATH: ${{ github.workspace }}
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
python3 -m pytest tests/ -q
|
||||||
132
docs/PRODUCT_VISION.md
Normal file
132
docs/PRODUCT_VISION.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Product Vision — Автономная фабрика разработки (Orchestrator)
|
||||||
|
|
||||||
|
> Мультиагентная платформа, которая превращает идею или баг в задеплоенный на прод результат — автономно, надёжно и дёшево.
|
||||||
|
|
||||||
|
**Версия:** 1.0 · **Дата:** 2026-06-04 · **Статус:** концепция развития
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Зачем это (бизнес-взгляд)
|
||||||
|
|
||||||
|
### Проблема
|
||||||
|
Классическая разработка — это люди-бутылочное-горлышко на каждом шаге: аналитик, архитектор, разработчик, ревьюер, тестировщик, деплой-инженер. Каждая передача задачи между ними — потеря времени, контекста и денег. Мелкая фича или баг едут днями.
|
||||||
|
|
||||||
|
### Решение
|
||||||
|
**Orchestrator** — это конвейер из ИИ-агентов, который проводит задачу через все стадии разработки сам: от бизнес-постановки до релиза на прод. Человек ставит задачу и принимает результат. Всё между — автономно.
|
||||||
|
|
||||||
|
### Ценность
|
||||||
|
- ⚡ **Скорость:** фича проходит полный цикл (анализ → архитектура → код → ревью → тесты → деплой) за ~35 минут без ручных вмешательств.
|
||||||
|
- 💰 **Стоимость:** работа агентов в разы дешевле команды; адаптивный выбор моделей экономит на простых задачах.
|
||||||
|
- 🎯 **Автономность:** 0 ручных пинков в штатном прогоне. Человек — постановщик и приёмщик, а не оператор.
|
||||||
|
- 🛡️ **Надёжность:** многоуровневые гейты качества не пускают недоделку на прод.
|
||||||
|
- 🔁 **Масштаб:** одна платформа ведёт несколько проектов; саму платформу можно тиражировать на новые хосты.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Как это работает (обзор)
|
||||||
|
|
||||||
|
### Конвейер
|
||||||
|
```
|
||||||
|
created → analysis → architecture → development → review → testing → deploy → done
|
||||||
|
```
|
||||||
|
На каждом переходе стоит **quality gate** — автоматическая проверка, которая не пускает задачу дальше, пока стадия не выполнена честно:
|
||||||
|
|
||||||
|
| Переход | Гейт | Что проверяет |
|
||||||
|
|---|---|---|
|
||||||
|
| analysis → architecture | check_analysis_approved | BRD/TRZ/AC готовы + апрув человека |
|
||||||
|
| architecture → development | check_architecture_done | Архитектура/ADR зафиксированы |
|
||||||
|
| development → review | check_ci_green | CI зелёный (тесты проходят) |
|
||||||
|
| review → testing | check_reviewer_verdict | Машинный вердикт ревьюера: APPROVED |
|
||||||
|
| testing → deploy | check_tests_passed | Машинный вердикт тестера (не подделать) |
|
||||||
|
| deploy → done | check_deploy_status | Деплой реально успешен, лог в origin/main |
|
||||||
|
|
||||||
|
### Агенты
|
||||||
|
- **Analyst** — собирает бизнес-требования, пишет BRD/TRZ/критерии приёмки.
|
||||||
|
- **Architect** — проектирует решение, фиксирует ADR.
|
||||||
|
- **Developer** — пишет код в изолированном git-worktree.
|
||||||
|
- **Reviewer** — ревьюит, выносит машинный вердикт.
|
||||||
|
- **Tester** — прогоняет тесты, фиксирует результат в отчёте.
|
||||||
|
- **Deployer** — мержит, тегирует, деплоит на прод, пишет deploy-log.
|
||||||
|
|
||||||
|
### Объекты
|
||||||
|
- **Project** — проект в реестре (Plane project ↔ git-репозиторий ↔ префикс задач).
|
||||||
|
- **Work-Item** — задача, проходящая конвейер; на каждой стадии накапливает артефакты (00-business-request … 14-deploy-log).
|
||||||
|
- **Job** — единица работы в очереди (atomic claim, ретраи, restart-safe).
|
||||||
|
|
||||||
|
### Интеграции
|
||||||
|
- **Plane** — управление задачами, статусы как триггеры конвейера, webhooks.
|
||||||
|
- **Gitea** — репозитории, PR, защита main (pre-receive hook).
|
||||||
|
- **Telegram** — живой трекер прогресса, апрувы, уведомления.
|
||||||
|
- **LLM** — модели агентов (сейчас Claude, в планах мультипровайдерность).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Что уже сделано (фундамент)
|
||||||
|
|
||||||
|
✅ **Автономный конвейер** — подтверждён живым прогоном: задача от issue до Done без ручных вмешательств (~35 мин).
|
||||||
|
✅ **Очередь задач** — atomic claim, max_concurrency, ретраи, restart-safe.
|
||||||
|
✅ **Изоляция через git-worktree** — каждая задача в своём дереве, без конфликтов в shared-репо.
|
||||||
|
✅ **Машинные гейты качества** — вердикты читаются из структурированных артефактов, а не угадываются по тексту.
|
||||||
|
✅ **Multi-repo** — платформа ведёт несколько проектов (enduro-trails, сам orchestrator).
|
||||||
|
✅ **Идемпотентность webhooks** — дедуп по delivery-id, защита от дублей.
|
||||||
|
✅ **Наблюдаемость** — учёт токенов и стоимости каждой задачи.
|
||||||
|
✅ **Живой Telegram-трекер** — прогресс редактируется в одном сообщении, без спама.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Куда движемся (дорожная карта)
|
||||||
|
|
||||||
|
Развитие сгруппировано в 5 стратегических направлений.
|
||||||
|
|
||||||
|
### 🛡️ Надёжность и безопасность
|
||||||
|
- **Post-deploy мониторинг + авто-rollback** — следить за продом после релиза, откатывать при деградации.
|
||||||
|
- **Security-гейт** — secret-scanning + аудит зависимостей перед мержем.
|
||||||
|
- **Бюджетный circuit-breaker** — хард-лимит стоимости на задачу, защита от «убегающих» расходов.
|
||||||
|
- **Опциональная human-приёмка** — финальный взгляд человека для критичных фич.
|
||||||
|
|
||||||
|
### 💰 Экономика и интеллект
|
||||||
|
- **Мультипровайдерность LLM** — Claude, OpenRouter, другие провайдеры на выбор.
|
||||||
|
- **Оценка задачи** — прогноз стоимости/времени до старта.
|
||||||
|
- **Адаптивный выбор модели** — по сложности: тривиальное на дешёвой, сложное на сильной.
|
||||||
|
- **Багфикс-трек** — упрощённый дешёвый путь для багов (без потери качества).
|
||||||
|
|
||||||
|
### 🏗️ Платформа и масштаб
|
||||||
|
- **Self-hosting** — оркестратор пилит сам себя через собственный конвейер.
|
||||||
|
- **Саморазвитие** — петля уроков: ловить отклонения → фиксировать → предлагать улучшения.
|
||||||
|
- **Онбординг проектов** — turnkey-заведение нового проекта в систему.
|
||||||
|
- **Тиражирование** — развернуть платформу на новой инфраструктуре под ключ.
|
||||||
|
|
||||||
|
### 💬 Взаимодействие с человеком
|
||||||
|
- **UX/UI дизайнер** — макеты интерфейсов на этапе аналитики.
|
||||||
|
- **Интерактивный аналитик** — живой диалог для уточнения требований и обсуждения макетов.
|
||||||
|
- **Единые коммент-артефакты** — все агенты прикладывают результаты с кликабельными ссылками.
|
||||||
|
- **Прямые ссылки в Telegram** — апрув в один клик, без блужданий.
|
||||||
|
|
||||||
|
### 🧩 Расширение возможностей
|
||||||
|
- **Тяжёлые расчёты данных** — опциональная стадия для миграций/обработки больших данных.
|
||||||
|
- **Android-разработка** — мобильный стек через тот же конвейер.
|
||||||
|
- **Декомпозиция эпиков** — большая фича → подзадачи → сборка.
|
||||||
|
- **Управление зависимостями** — задача B ждёт задачу A.
|
||||||
|
- **Code coverage gate** — защита покрытия тестами от деградации.
|
||||||
|
- **База знаний проекта** — персистентный контекст для агентов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Принципы (что для нас неизменно)
|
||||||
|
|
||||||
|
1. **Автономность по умолчанию, человек — на ключевых развилках.** Машина делает, человек ставит и принимает.
|
||||||
|
2. **Качество не приносится в жертву скорости/цене.** Удешевляем аналитику — гейты качества остаются. Урок дорого выученный: срезанная проверка = недоделка на проде.
|
||||||
|
3. **Машинные вердикты, а не угадывание.** Гейты читают структурированные поля, а не ищут слова в тексте.
|
||||||
|
4. **Самоизменение — только через PR + ревью + апрув.** Агент, меняющий агентов, всегда под контролем человека.
|
||||||
|
5. **Документация — сразу, не потом.** Изменил функционал → обновил доки.
|
||||||
|
6. **Прод — источник правды.** «Деплой прошёл» ≠ «работает». Проверяем реальный результат.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Видение в одну фразу
|
||||||
|
|
||||||
|
> **Самодостаточная фабрика разработки, которая размножается, учится на ошибках, оценивает себя, бережёт бюджет и не ломает прод — превращая намерение человека в работающий продукт почти без его участия.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Документ поддерживается в репозитории orchestrator. Источник дорожной карты — задачи проекта ORCH в Plane (ORCH-7…ORCH-28).*
|
||||||
BIN
docs/PRODUCT_VISION.pptx
Normal file
BIN
docs/PRODUCT_VISION.pptx
Normal file
Binary file not shown.
@@ -38,3 +38,36 @@ def _no_telegram(monkeypatch):
|
|||||||
monkeypatch.setattr("src.agents.launcher.send_telegram", _noop, raising=False)
|
monkeypatch.setattr("src.agents.launcher.send_telegram", _noop, raising=False)
|
||||||
monkeypatch.setattr("src.queue_worker.send_telegram", _noop, raising=False)
|
monkeypatch.setattr("src.queue_worker.send_telegram", _noop, raising=False)
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _reset_webhook_secrets(monkeypatch):
|
||||||
|
"""Isolate settings singleton between test files (CI cross-file isolation).
|
||||||
|
|
||||||
|
settings is a process-wide Pydantic singleton read once at import. Different
|
||||||
|
test modules set env variables differently at import-time, so those values leak
|
||||||
|
across files when pytest collects them together (as CI does).
|
||||||
|
|
||||||
|
1. webhook secrets: reset to "" so HMAC is disabled by default. Tests that
|
||||||
|
intentionally test the 401 path (test_webhook_dedup.py:268,278) re-apply
|
||||||
|
their own monkeypatch AFTER this autouse fixture runs, which overrides the
|
||||||
|
reset for the duration of that one test only.
|
||||||
|
|
||||||
|
2. db_path: reset to the value from ORCH_DB_PATH env var (last written by the
|
||||||
|
last imported test module). Without this, test_webhook_dedup.py (imported
|
||||||
|
first, alphabetically) seeds settings.db_path = dedup.db, while
|
||||||
|
test_webhooks.py's setup_db fixture tries to remove test_orchestrator.db,
|
||||||
|
leaving the DB dirty across tests that share a branch name and causing
|
||||||
|
get_task_by_repo_branch() to return a stale row with the wrong stage.
|
||||||
|
Per-test monkeypatches in test_webhook_dedup.setup_db override this reset.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from src.webhooks import gitea as gitea_mod
|
||||||
|
from src.webhooks import plane as plane_mod
|
||||||
|
from src import db as db_mod
|
||||||
|
monkeypatch.setattr(gitea_mod.settings, "gitea_webhook_secret", "", raising=False)
|
||||||
|
monkeypatch.setattr(plane_mod.settings, "plane_webhook_secret", "", raising=False)
|
||||||
|
db_path_env = os.environ.get("ORCH_DB_PATH", "")
|
||||||
|
if db_path_env:
|
||||||
|
monkeypatch.setattr(db_mod.settings, "db_path", db_path_env, raising=False)
|
||||||
|
yield
|
||||||
|
|||||||
@@ -54,13 +54,19 @@ def test_status_endpoint():
|
|||||||
assert "active_tasks" in resp.json()
|
assert "active_tasks" in resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.plane_sync.add_comment")
|
||||||
|
@patch("src.plane_sync.fetch_issue_sequence_id", return_value=None)
|
||||||
|
@patch("src.plane_sync.fetch_issue_fields", return_value=("Test task", "This is a detailed test description for the task"))
|
||||||
@patch("src.webhooks.plane._create_gitea_branch", new_callable=AsyncMock)
|
@patch("src.webhooks.plane._create_gitea_branch", new_callable=AsyncMock)
|
||||||
@patch("src.webhooks.plane._create_initial_docs", new_callable=AsyncMock)
|
@patch("src.webhooks.plane._create_initial_docs", new_callable=AsyncMock)
|
||||||
def test_plane_webhook_creates_task(mock_docs, mock_branch):
|
def test_plane_webhook_creates_task(mock_docs, mock_branch, mock_fetch_fields, mock_fetch_seq, mock_add_comment):
|
||||||
"""work_item.created → task in DB with stage=analysis."""
|
"""work_item.created (via In Progress status) → task in DB with stage=analysis."""
|
||||||
resp = client.post("/webhook/plane", json={
|
resp = client.post("/webhook/plane", json={
|
||||||
"event": "work_item.created",
|
"event": "issue", "action": "updated",
|
||||||
"data": {"id": "test-123", "name": "Test task", "project": "proj-1"}
|
"data": {
|
||||||
|
"id": "test-123", "name": "Test task", "project": "proj-1",
|
||||||
|
"state": {"id": "b873d9eb-993c-48cd-97ac-99a9b1623967", "name": "In Progress", "group": "started"},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.json()["status"] == "accepted"
|
assert resp.json()["status"] == "accepted"
|
||||||
@@ -75,17 +81,37 @@ def test_plane_webhook_creates_task(mock_docs, mock_branch):
|
|||||||
assert "feature/" in task["branch"]
|
assert "feature/" in task["branch"]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.plane_sync.add_comment")
|
||||||
|
@patch("src.plane_sync.fetch_issue_sequence_id", return_value=None)
|
||||||
|
@patch("src.plane_sync.fetch_issue_fields",
|
||||||
|
side_effect=[
|
||||||
|
("First task", "This is a detailed description for the first task item"),
|
||||||
|
("Second task", "This is a detailed description for the second task item"),
|
||||||
|
])
|
||||||
@patch("src.webhooks.plane._create_gitea_branch", new_callable=AsyncMock)
|
@patch("src.webhooks.plane._create_gitea_branch", new_callable=AsyncMock)
|
||||||
@patch("src.webhooks.plane._create_initial_docs", new_callable=AsyncMock)
|
@patch("src.webhooks.plane._create_initial_docs", new_callable=AsyncMock)
|
||||||
def test_plane_webhook_generates_sequential_ids(mock_docs, mock_branch):
|
def test_plane_webhook_generates_sequential_ids(
|
||||||
"""Multiple work items get sequential IDs."""
|
mock_docs, mock_branch, mock_fetch_fields, mock_fetch_seq, mock_add_comment
|
||||||
|
):
|
||||||
|
"""Multiple In Progress transitions get sequential IDs (ET-001, ET-002)."""
|
||||||
|
in_progress_state = {
|
||||||
|
"id": "b873d9eb-993c-48cd-97ac-99a9b1623967",
|
||||||
|
"name": "In Progress",
|
||||||
|
"group": "started",
|
||||||
|
}
|
||||||
client.post("/webhook/plane", json={
|
client.post("/webhook/plane", json={
|
||||||
"event": "work_item.created",
|
"event": "issue", "action": "updated",
|
||||||
"data": {"id": "item-1", "name": "First task", "project": "proj-1"}
|
"data": {
|
||||||
|
"id": "item-1", "name": "First task", "project": "proj-1",
|
||||||
|
"state": in_progress_state,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
client.post("/webhook/plane", json={
|
client.post("/webhook/plane", json={
|
||||||
"event": "work_item.created",
|
"event": "issue", "action": "updated",
|
||||||
"data": {"id": "item-2", "name": "Second task", "project": "proj-1"}
|
"data": {
|
||||||
|
"id": "item-2", "name": "Second task", "project": "proj-1",
|
||||||
|
"state": in_progress_state,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -202,8 +228,9 @@ def test_gitea_webhook_push():
|
|||||||
assert resp.json()["status"] == "accepted"
|
assert resp.json()["status"] == "accepted"
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.webhooks.gitea.plane_notify_stage")
|
||||||
@patch("src.webhooks.gitea.launcher")
|
@patch("src.webhooks.gitea.launcher")
|
||||||
def test_gitea_push_with_adr_advances_stage(mock_launcher):
|
def test_gitea_push_with_adr_advances_stage(mock_launcher, mock_plane_notify):
|
||||||
"""Push with ADR files at architecture stage → advance to development."""
|
"""Push with ADR files at architecture stage → advance to development."""
|
||||||
mock_launcher.launch.return_value = 1
|
mock_launcher.launch.return_value = 1
|
||||||
|
|
||||||
@@ -235,7 +262,7 @@ def test_gitea_push_with_adr_advances_stage(mock_launcher):
|
|||||||
task = conn.execute("SELECT * FROM tasks WHERE plane_id = 'push-001'").fetchone()
|
task = conn.execute("SELECT * FROM tasks WHERE plane_id = 'push-001'").fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
assert task["stage"] == "development"
|
assert task["stage"] == "development"
|
||||||
mock_launcher.launch.assert_called_once()
|
mock_plane_notify.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
@patch("src.webhooks.gitea.check_ci_green")
|
@patch("src.webhooks.gitea.check_ci_green")
|
||||||
|
|||||||
Reference in New Issue
Block a user