Files
orchestrator/docs/architecture/internals.md
dev 8a292b9d33
All checks were successful
CI / test (pull_request) Successful in 12s
feat(agents): configurable LLM model + effort per-agent and per-project (ORCH-41)
Vынести модель/effort агентов из хардкода launcher.py в конфиг.
- config.py: ORCH_AGENT_MODEL_<AGENT>/_DEFAULT (default claude-opus-4-8),
  ORCH_AGENT_EFFORT_<AGENT>/_DEFAULT (думающие=high, tester/deployer=medium),
  ORCH_AGENT_FALLBACK_MODEL.
- projects.py: ProjectConfig.agent_models/agent_efforts (field(default_factory=dict)),
  парсинг из projects_json через _coerce_str_map.
- launcher.py: resolve_agent_model/resolve_agent_effort (project>env>default>пусто),
  валидация effort {low,medium,high,xhigh,max}; убран хардкод model:opus;
  собираются флаги --model/--effort/--fallback-model.
- tests: test_resolve_agent_model.py, test_resolve_agent_effort.py.
- docs: INFRA.md, internals.md, CHANGELOG.md.
2026-06-05 16:16:57 +00:00

20 KiB
Raw Blame History

Архитектура Orchestrator

Обзор

Orchestrator — event-driven FastAPI сервис, который управляет жизненным циклом задач разработки через мульти-агентный пайплайн. Каждая задача проходит через фиксированные стадии, на каждой из которых работает специализированный Claude CLI агент.

Компоненты

1. Webhook Receivers

Plane Webhook (src/webhooks/plane.py)

  • Фильтр по проекту (ORCH-6): извлекает data.project (Plane project uuid) и игнорирует событие, если проект не в реестре (known_plane_project_ids()) → ответ {"status":"ignored","reason":"unknown project"}. Это предотвращает инцидент 2026-06-02 (webhook на весь workspace без фильтра).
  • Принимает work_item.created — резолвит repo/prefix/Plane-проект из реестра по project, создаёт задачу в DB, запускает analyst
  • Принимает work_item.updated — синхронизация статусов

Реестр проектов (src/projects.py, multi-repo, ORCH-6)

Маппинг Plane project id → (repo, work_item_prefix, name). Позволяет одному оркестратору обслуживать несколько репозиториев, не путая их.

@dataclass(frozen=True)
class ProjectConfig:
    plane_project_id: str   # uuid Plane-проекта (ключ реестра)
    repo: str               # имя gitea-репо (= папка в /repos)
    work_item_prefix: str   # ET / ORCH
    name: str               # человекочитаемое

Резолверы:

  • get_project_by_plane_id(uuid) -> ProjectConfig | None — для фильтра/резолва в plane-webhook.
  • get_project_by_repo(repo) -> ProjectConfig | None — когда известен только repo (gitea-webhook, plane_sync).
  • known_plane_project_ids() -> set[str] — множество разрешённых проектов (фильтр).

Источник конфигурации: env ORCH_PROJECTS_JSON (JSON-массив ProjectConfig). Если пусто/битый JSON — используется встроенный дефолт-реестр (enduro-trails + orchestrator), чтобы система работала из коробки. Парсинг устойчив: битые записи пропускаются, полностью невалидный JSON → fallback на дефолт.

Следствия multi-repo:

  • repo per project: repo = get_project_by_plane_id(project_id).repo вместо хардкода default_repo.
  • prefix per project: get_next_work_item_id(repo, prefix) нумерует независимо — ORCH-001 vs ET-010 (src/db.py).
  • plane_sync в правильный проект: state/comment пишутся в Plane-проект самой задачи (резолв по repo через get_project_by_repo), а не в единственный хардкоженный PROJECT_ID (обратная совместимость сохранена дефолтом на enduro).
  • gitea-webhook: push в repo вне реестра → ignored (не триггерит конвейер).

Gitea Webhook (src/webhooks/gitea.py)

  • push — проверяет наличие артефактов (docs/, src/), продвигает стадию
  • pull_request* (wildcard) — обрабатывает review approved/rejected, PR merge
  • status — CI green/failure, продвигает development → review

2. State Machine (src/stages.py)

Линейный пайплайн с одним возможным откатом (review → development):

STAGE_TRANSITIONS = {
    created:      → analysis      (agent: None)
    analysis:     → architecture  (agent: architect,  QG: check_analysis_approved)
    architecture: → development   (agent: developer,  QG: check_architecture_done)
    development:  → review        (agent: reviewer,   QG: check_tests_local)
    review:       → testing       (agent: tester,     QG: check_reviewer_verdict)
    testing:      → deploy-staging (agent: deployer,   QG: check_tests_passed)
    deploy-staging: → deploy      (agent: deployer,   QG: check_staging_status)
    deploy:       → done          (agent: None,       QG: None)
}

3. Quality Gates (src/qg/checks.py)

Check Метод проверки
check_analysis_approved Filesystem: 4 файла + :approved: comment в Plane
check_architecture_done Filesystem: ADR dir или infra-requirements.md
check_tests_local Оркестратор сам гоняет make test в worktree задачи /repos/_wt/<repo>/<branch> (judge по exit-code). Заменил check_ci_green: Gitea CI не сконфигурирован. Worktree-изоляция → безопасно при параллельных задачах (ORCH-2 / S-4).
check_reviewer_verdict Filesystem: читает verdict: APPROVED|REQUEST_CHANGES из YAML-frontmatter 12-review.md (только машиночитаемое поле, не подстроки в тексте)
check_tests_passed Filesystem: test-report.md содержит "PASS"
check_ci_green (legacy) Gitea API: GET /commits/{branch}/status — больше не используется как QG развития
check_review_approved (legacy) Gitea API: GET /pulls/{n}/reviews — не используется в STAGE_TRANSITIONS

4. Agent Launcher (src/agents/launcher.py)

Запускает Claude CLI как subprocess:

claude.exe --print  --system-prompt  --allowedTools Read,Write,Edit,Bash

Каждый запуск:

  1. Записывает run в DB (agent_runs)
  2. Запускает subprocess. stdout/stderr перенаправляются СРАЗУ в файл /app/data/runs/{id}.log на уровне ОС (Popen stdout=log_fh). Никакого PIPE в памяти оркестратора → нет PIPE-deadlock, нет потока-читателя, нет зомби (B-2).
  3. Стартует watchdog thread (timeout 30 мин → SIGKILL по pid)
  4. Стартует monitor thread: proc.wait() (гарантированный reap → реальный exit_code в БД) → закрывает log_fh → git commit/push → auto-advance

5. Auto-advance (launcher._try_advance_stage)

После успешного завершения агента:

  1. Определяет текущую стадию задачи
  2. Проверяет QG для выхода из стадии
  3. Если QG пройден — продвигает стадию
  4. Запускает следующего агента (если определён)

Примечание: переход review → testing использует check_reviewer_verdict (читается из frontmatter 12-review.md); development → reviewcheck_tests_local (оркестратор сам прогоняет тесты, не зависит от Gitea CI).

6. Review Bounce

При REQUEST_CHANGES:

  1. Считает количество developer runs для задачи
  2. Если < MAX_DEV_RETRIES (3) — откатывает в development, перезапускает developer
  3. Если >= MAX_DEV_RETRIES — эскалация (логирование + уведомление)

Database Schema

-- Задачи
CREATE TABLE tasks (
    id INTEGER PRIMARY KEY,
    work_item_id TEXT,          -- Plane issue identifier (e.g. "ET-006")
    plane_issue_id TEXT,        -- Plane UUID
    repo TEXT,
    branch TEXT,
    stage TEXT DEFAULT 'created',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Запуски агентов
CREATE TABLE agent_runs (
    id INTEGER PRIMARY KEY,
    task_id INTEGER REFERENCES tasks(id),
    agent TEXT,                 -- analyst/architect/developer/reviewer/tester
    started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    finished_at TIMESTAMP,
    exit_code INTEGER,
    output_path TEXT            -- /app/data/runs/{id}.log
);

-- Сырые события
CREATE TABLE events (
    id INTEGER PRIMARY KEY,
    source TEXT,               -- plane/gitea
    event_type TEXT,
    payload TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Deployment

Docker Compose

services:
  orchestrator:
    build: .
    container_name: orchestrator
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./data:/app/data                    # SQLite + logs
      - /home/slin/repos:/repos             # Git repositories
      - /var/run/docker.sock:/var/run/docker.sock  # Docker CLI
      - claude-code:/opt/claude-code:ro     # Claude CLI binary
      - /home/slin/.claude:/home/slin/.claude      # Claude config
    env_file: .env
    group_add: ["999"]         # docker group

Dockerfile

  • Base: python:3.12-slim
  • Docker CLI (sibling containers)
  • tini как PID 1 (proper zombie reaping)
  • git config --global safe.directory '*'
  • ENTRYPOINT: tini → uvicorn

Потоки данных

Happy path (ET-006 пример)

1. Plane webhook: work_item.created → task created, analyst launched
2. Analyst: пишет BRD/TRZ/AC/TestPlan → git push docs/
3. Plane comment :approved: → QG check_analysis_approved → PASS
4. Auto-advance: analysis → architecture, architect launched
5. Architect: пишет ADR, infra-requirements → git push docs/
6. Gitea push webhook: ADR detected → QG check_architecture_done → PASS
7. Auto-advance: architecture → development, developer launched
8. Developer: пишет код src/ + tests/ → git push, creates PR
9. Gitea status webhook: CI green → QG check_ci_green → PASS
10. Auto-advance: development → review, reviewer launched
11. Reviewer: оставляет review (APPROVED или REQUEST_CHANGES)
12. Gitea PR webhook: review event → QG check_review_approved → PASS
13. Advance: review → testing, tester launched
14. Tester: прогоняет тесты, пишет test-report.md → git push
15. Auto-advance: testing → deploy-staging (QG check_tests_passed → PASS)
16. Deployer: runs staging checks → writes 15-staging-log.md (staging_status: SUCCESS)
17. Auto-advance: deploy-staging → deploy (QG check_staging_status → PASS)
18. PR merge → Gitea PR webhook: action=closed, merged=true → done

Review bounce path

11. Reviewer: REQUEST_CHANGES
12. Gitea PR webhook: review_state=REQUEST_CHANGES, stage=review
13. Rollback: review → development, developer relaunched (attempt N/3)
14. Developer: фиксит замечания → git push
15. CI green → development → review, reviewer relaunched
16. Reviewer: APPROVED → continue happy path

Resilience

Механизм Описание
Watchdog Каждый агент: timeout 30 мин → SIGKILL + exit_code=-9
safe.directory git операции работают в любой директории
Max retries Developer: max 3 попытки, затем эскалация
Zombie-free stdout идёт сразу в файл + monitor proc.wait() → процесс всегда reap'нут (B-2)
Orphan recovery При старте: orphan-run'ы (finished_at IS NULL, старше 35 мин) помечаются exit=-1 с per-run warning + Telegram-уведомлением «нужна ручная проверка» (M-1)

Агенты

Каждый агент — Claude CLI с:

  • System prompt: .openclaw/agents/{role}.md (в репозитории)
  • Task file: .task-{suffix}.md — генерируется orchestrator прямой записью в worktree задачи /repos/_wt/<repo>/<branch>/ (B-1, без docker; ORCH-2 — в изолированную рабочую копию, не в shared /repos/<repo>). В .gitignore репозитория проекта (рантайм-артефакт, не коммитится).
  • Tools: Read, Write, Edit, Bash
  • Output: --print mode (весь вывод в stdout после завершения)
Агент Артефакты Время (типичное)
analyst BRD, TRZ, AC, TestPlan 5-10 мин
architect ADR, infra-requirements, tech-risks 5-10 мин
developer src/, tests/, PR 15-30 мин
reviewer review report, PR review 3-5 мин
tester test-report.md, e2e results 10-25 мин
deployer merge PR + SSH deploy-hook + smoke 5-10 мин

Изоляция через git worktree (ORCH-2 / S-4)

Каждая задача (= одна git-ветка) работает в изолированной git worktree, а не в общем /repos/<repo>. Это убирает гонки git checkout, когда две задачи активны одновременно.

/repos/<repo>                      ← основной clone (fetch / управление worktree, read-only запросы)
/repos/_wt/<repo>/<safe-branch>    ← worktree конкретной задачи (рабочая копия агента)

Модуль src/git_worktree.py:

  • get_worktree_path(repo, branch) — путь worktree (не создаёт).
  • ensure_worktree(repo, branch) — создаёт (или переиспользует) worktree на нужной ветке; для новой ветки создаёт её от origin/main. Возвращает путь.
  • remove_worktree(repo, branch) — опциональная очистка при done.

Где используется worktree:

  • launcher: агент запускается с cd <worktree> (без git checkout в cmd); task-файл пишется в worktree; commit/push в _monitor_agent идут в worktree.
  • qg/checks: чтение артефактов агента (check_analysis_complete, check_architecture_done, check_tests_passed, check_reviewer_verdict) и check_tests_local (make test) — из worktree. Артефакт-функции принимают опциональный branch; без него падают на shared /repos/<repo> (обратная совместимость).
  • webhooks/gitea: git branch -r --contains <sha> оставлен в основном clone — это read-only запрос (нет checkout/мутации), гонок не создаёт.

Один branch может быть checked out только в одной worktree одновременно — это и есть нужное свойство: одна задача = одна ветка = одна worktree.

Известные ограничения

  • Shared /repos checkout (гонки при параллельных задачах). РЕШЕНО (ORCH-2 / S-4): git worktree per task/branch — см. раздел «Изоляция через git worktree» ниже.
  • In-process daemon-потоки (рестарт → сироты, потеря работы). РЕШЕНО (ORCH-1 / F-2b): персистентная очередь jobs + фоновый воркер — см. раздел «Очередь задач (ORCH-1)» ниже. Daemon-потоки monitor/watchdog остаются для одного запущенного агента, но при рестарте его job возвращается в queued (queue-recovery) и переподхватывается.

Очередь задач (ORCH-1 / F-2b)

Раньше webhook-хэндлер синхронно спавнил subprocess.Popen + 2 daemon-thread прямо в процессе uvicorn (8 точек вызова). Рестарт = сироты + потеря работы, нет лимита параллелизма, нет ретраев.

Flow

webhook (plane/gitea)                 background thread (queue_worker)
        │                                        │
  enqueue_job() ---> [ jobs table ] <--- claim_next_job()  (atomic queued->running)
  (мгновенный          status=queued                 │
   ответ 200)                                    launch_job(job)
                                                       │
                                          AgentLauncher._spawn (Popen claude)
                                                       │
                                          _monitor_agent (proc.wait, commit/push,
                                                       │  advance stage)
                                                       │
                                          _finalize_job:
                                            exit 0  -> mark_job done
                                            exit !=0 & attempts<max -> requeue (queued)
                                            exit !=0 & attempts>=max -> failed + Telegram

Таблица jobs

Колонка Назначение
status queuedrunningdone | failed
attempts / max_attempts счётчик попыток (инкремент при claim) / лимит ретраев (default 2)
run_id FK на agent_runs.id после старта
task_content ТЗ, которое пишется в task-файл агента
error последняя ошибка

idx_jobs_status (status, id) — быстрый FIFO-выбор queued.

Атомарный claim

claim_next_job() делает SELECT queued ORDER BY id LIMIT 1UPDATE ... WHERE id=? AND status='queued' и проверяет rowcount. При гонке двух тиков лишь один UPDATE переведёт строку в running (rowcount==1); проигравший берёт следующий job.

Queue-recovery (рестарт-safe)

В main.py lifespan после M-1 orphan-recovery вызывается requeue_running_jobs(): jobs со статусом running (воркер умёр на рестарте) → возвращаются в queued. Потом стартует воркер; на shutdown — worker.stop() (Event.set + join).

Конфиг

  • ORCH_MAX_CONCURRENCY (default 1) — лимит параллельных jobs.
  • ORCH_QUEUE_POLL_INTERVAL (default 2.0) — период опроса.
  • ORCH_AGENT_MODEL_DEFAULT / ORCH_AGENT_MODEL_<AGENT> (ORCH-41) — модель агентов; дефолт claude-opus-4-8.
  • ORCH_AGENT_EFFORT_DEFAULT / ORCH_AGENT_EFFORT_<AGENT> (ORCH-41) — режим --effort (low|medium|high|xhigh|max).
  • ORCH_AGENT_FALLBACK_MODEL (ORCH-41) — опц. --fallback-model при overloaded.
  • per-project override: agent_models / agent_efforts в ORCH_PROJECTS_JSON; резолверы resolve_agent_model / resolve_agent_effort (project > per-agent env > default > пусто).

Наблюдаемость: GET /queue — counts по статусам + последние 10 jobs.

Совместимость: launcher.launch() (прямой синхронный запуск, job_id=None) сохранён для обратной совместимости. Очередь использует launch_job(); оба разделяют _spawn() (Popen-логика B-2 не изменена).

  • Gitea CI не настроен. QG развития теперь локальный (check_tests_local); Gitea CI-статусы не являются authoritative и не блокируют pipeline.
  • Docker внутри контейнера orchestrator НЕДОСТУПЕН. Деплой идёт только через SSH-хук enduro-deploy-hook.sh на хосте.