Files
ratatoskr-go/README.md
ki.sagidullin 8c91c83024
Some checks failed
CI / test (push) Failing after 1m20s
CI / build-and-package (amd64, linux) (push) Failing after 1m4s
CI / build-and-package (amd64, windows) (push) Successful in 37s
fix(opencode): агенты — в <worktree>/.opencode/agent, OPENCODE_CONFIG_DIR больше не выставляем
OPENCODE_CONFIG_DIR в opencode v1.18.18 перенаправляет Global.Path.config
(global.ts: config = OPENCODE_CONFIG_DIR ?? ~/.config/opencode), из-за чего
глобальный конфиг (модель/провайдеры, напр. tokentool) не загружался и
opencode уходил в fallback-модель. Агенты открывались, т.к. OPENCODE_CONFIG_DIR
дополнительно сканируется как каталог для agent/*.md.

Теперь агенты распаковываются в <worktree>/.opencode/agent/*.md, где opencode
находит их через project-каталог .opencode (paths.ts, cwd=worktree). Глобальный
конфиг не трогаем вовсе.

- internal/opencode: удалены Server.ConfigDir/Pool.ConfigDir и env OPENCODE_CONFIG_DIR
- internal/config: удалено поле OpenCodeCfg.ConfigDir (config_dir)
- internal/app: ensureAgentsDir пишет в <worktree>/.opencode/agent
- internal/agents: WriteTo(dir) → dir/agent/*.md
- README/config.yaml.example/memory обновлены
2026-08-23 17:54:35 +05:00

258 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ratatoskr-go
Ratatoskr на Go — **оркестратор в единый статический бинарь** + субагенты через
[opencode](https://opencode.ai) как внешний процесс. Переезд с Python-версии:
все подсистемы (диалог, воркер, автообновление) собраны в один исполняемый
файл без runtime-зависимостей (`CGO_ENABLED=0`), что упрощает доставку и деплой.
## Структура
```
cmd/ratatoskr/ # точка входа, сборка бинаря (App.New + Run)
internal/
app/ # компоновка (composition root), config, DI, packageOwner
config/ # загрузка YAML+env: ε подстановка ${VAR:-default}, defaults, validate (классы C1C4)
chat/ # мультиканальный Router; telegram — long-poll канал
core/ # state-machine задач: /start /cancel /skip /retry N /status N /continue N
analyst/ # аналитик: промпт + разбор JSON-решения (opencode agent, классы A1A4)
worker/ # polling-планировщик + dev/reviewer-конвейер (классы W*, E*, R*)
agents/ # встроенные агенты opencode (analyst.md, dev.md, reviewer.md) через go:embed
opencode/ # HTTP-клиент v2 API opencode serve, поллинг вердикта (классы O1O5)
storage/ # SQLite (modernc.org/sqlite, без CGO): tasks, traces, task_history (S1S5)
update/ # автообновление из Gitea Packages (классы U1U6)
```
## Сборка
Требуется Go (на хосте без `go` в PATH — экспорт вручную):
```bash
export PATH=/opt/data/.local/go/bin:$PATH # локальный Go-тулчейн
go version # go1.25.0 linux/amd64
```
### Обычная сборка (текущая платформа)
```bash
cd /opt/data/src/ratatoskr-go
make build # go build -ldflags="-s -w" -o ratatoskr ./cmd/ratatoskr/
./ratatoskr -version
```
### Кросс-сборка под linux/amd64 и windows/amd64 (только эти две платформы)
```bash
make cross
# → ratatoskr-linux-amd64
# → ratatoskr-windows-amd64.exe (Windows-машина Камиля)
```
`Makefile` вшивает в бинарь версию (ldflag):
```make
GIT_SHA := $(shell git rev-parse --short HEAD || echo dev)
GOFLAGS ?= -ldflags="-s -w -X main.version=commit-$(GIT_SHA) -X main.updateToken=$(UPDATE_TOKEN)"
```
- `main.version` = `commit-<sha7>` — та же версия, под которой бинарь публикуется в Gitea Packages.
- `main.updateToken` = токен read:package для автообновления (см. ниже). Если не задан
(`UPDATE_TOKEN ?= ""`) — пустая встройка, тогда обновление берёт `update.token` из конфига.
**Как на самом деле публикуется рабочий бинарь:** локальный `go build` токен НЕ вшивает
(значение секрета есть только у CI). Запушь `main` — CI (см. ниже) сам соберёт оба бинаря
со вшитым `TC_UPDATE_TOKEN` и загрузит их в Gitea Packages. Так соблюдён least-privilege:
read-токен не светится на хосте разработки.
### Проверки перед сборкой
```bash
make test # go test ./... -v -count=1 -timeout 120s
make vet # go vet ./...
```
### Прогон
```bash
make run # build + ./ratatoskr -config config.yaml
```
## CI (.gitea/workflows/ci.yaml)
- **Job `test`**: checkout → setup-go → `go test ./...``go vet ./...`.
- **Job `build-and-package`** (matrix linux/amd64 + windows/amd64, без артефактов):
собирает бинарь со вшитым `-X main.updateToken=${secrets.TC_UPDATE_TOKEN}` и
**публикует в Gitea Packages** — только на `main`, сразу в версию `commit-<sha7>/`
(псевдо-версия `latest` НЕ используется — см. автообновление). Плюс companion-файлы
`<file>.version` и `<file>.sha256`.
Секреты/vars: `TC_GITEA_TOKEN` (write:packages), `TC_UPDATE_TOKEN` (read:package),
`GIT_MAIN_URL`.
## Конфигурация (config.yaml)
Минимальный рабочий конфиг (порядок полей произвольный) и блок автообновления
(подробнее в `config.yaml.example`):
```yaml
telegram:
enabled: true # false — отключить Telegram (без сетевых вызовов к api.telegram.org)
token: "..." # TG_TOKEN (обязательно, если enabled)
chat_id: "..." # TG_CHAT_ID (обязательно, если enabled)
opencode:
bin: "opencode"
paths:
db: "./ratatoskr.db" # дефолт; резолвится от каталога бинаря (не от cwd)
worktree: "./worktrees" # то же правило «всё рядом с .exe»
update:
enabled: true
base_url: "http://gitea.hal9000.home" # env UPDATE_BASE_URL; пуст = обновление ОТКЛЮЧЕНО
token: "${UPDATE_TOKEN}" # env UPDATE_TOKEN — фоллбэк, если не вшит ldflag
package: "ratatoskr"
check_interval: 24h
```
**Пути «всё рядом с .exe»:** относительные `db`/`worktree` резолвятся к каталогу
бинаря (`config.ExeDir()`), а НЕ к cwd. `config.yaml` ищется сначала рядом с бинарём,
фоллбэк — cwd. Абсолютные пути не трогаются.
## Команды бота
| Команда | Описание |
|---|---|
| `/start` | начать новую задачу |
| `/cancel` | отменить активную задачу |
| `/skip` | пропустить шаг |
| `/retry N` | повторить задачу N |
| `/status N` | статус задачи N |
| `/continue N` | продолжить |
| `/update` | проверить + применить обновление из Gitea Packages |
| `/status` | версия бинаря + есть ли доступное обновление |
| `/help` | справка по всем командам |
## Интеграция с opencode (субагенты)
Субагенты (analyst / dev / reviewer / postmortem) запускаются через **headless** `opencode serve`
по **v2 HTTP API** (префикс `/api/*`). Требуемая версия opencode: **>= 1.18.18**
(сборки с v2 HTTP API). Старый бинарь, отвечающий только на `/global/health`,
не подходит: healthcheck падает с понятной ошибкой (класс O1).
Что делает обёртка (`internal/opencode`):
- **Модель — только глобальный конфиг opencode.** ratatoskr модель не выбирает
и про неё не знает: opencode сам берёт модель по умолчанию из своего
глобального конфига (`~/.config/opencode/opencode.jsonc`). Наш код конфиг
opencode не читает.
- **Свой агент.** При создании сессии в `POST /api/session` передаётся имя
встроенного агента ratatoskr (analyst/dev/reviewer/chat/postmortem). Агенты
распаковываются в `<worktree>/.opencode/agent/*.md`, где opencode находит
их через project-каталог `.opencode` (см. `internal/agents`); глобальный
конфиг при этом не трогается.
- **Поллинг вердикта.** Промпт отправляется неблокирующе (`POST .../prompt`
durable admit), вердикт собирается из новых assistant-сообщений
(`GET .../message`); завершение ответа — сессия ушла из активных дренажей
(`GET .../active`) и появилось финальное assistant-сообщение, стабильное
несколько опросов подряд.
## Фазы аналитика
Аналитик (`internal/analyst`) возвращает JSON-вердикт с полем `phase`:
| phase | Смысл | Требования валидатора |
|---|---|---|
| `ask` | данных не хватает — задаёт уточняющие вопросы | есть `chat_reply` или `questions` |
| `propose` | черновик достаточно заполнен, вносятся правки | хотя бы одно изменённое поле (`title`/`goal`/`repos`/`why`/`ac`) |
| `ready` | черновик уже полный и готов как есть — менять нечего | изменённых полей не требуется |
| `abort` | тема не про код / не подходит | опционально `abort_reason` |
`ready` был добавлен как явная фаза для случая, когда аналитик видит полностью готовый
черновик и просто запускает задачу агенту-кодеру (раньше модель не могла это выразить и
возвращала пустой `propose`, который валидатор резал как «нет изменённых полей»).
В `internal/core` фазы `propose` и `ready` обрабатываются одинаково (применить черновик,
проверить репозитории, поставить `ready` и отдать резюме).
## Постмортем после failed/timeout
Когда задача завершилась `failed` или `timeout`, воркер дополнительно запускает
**постмортем-анализ** (`internal/worker/postmortem.go`, agent `postmortem`):
- анализирует сессии dev/reviewer (промпты и выводы из `traces`);
- оценивает законченность этапов и причины сбоя;
- сохраняет результат как trace `agent=postmortem` и шлёт владельцу уведомление
«🔍 анализ (после <статус>): почему так случилось / что сделать».
Статус задачи постмортем не меняет; сбои самого анализа не влияют на исход задачи.
## Автообновление из Gitea Packages
Бинарь умеет сам себя обновлять из generic-пакета в Gitea. Модель:
**авто = только Check + уведомление** владельцу («доступна версия X, выполни /update»),
**применение — только по `/update`**.
- **Источник версий:** `{update.base_url}/api/packages/{owner}/generic/{package}`,
где `owner = "kamelion"` (константа `packageOwner` в коде), `package = "ratatoskr"`.
- **Каждая публикация — в конкретную версию `commit-<sha7>/`** (никакого `latest/`:
при параллельных PUT матрицы CI псевдо-версия `latest` разъезжается, и контрольные
суммы не сходятся — класс ошибки U4). Новейшая применимая версия ищется листингом:
фильтр `commit-*`, проба скачать бинарь платформы, берём версию с макс `id`.
- **Companion-файлы** рядом с бинарём в той же версии: `.<file>.version` (id версии)
и `<file>.sha256` (контрольная сумма). Verify всегда сверяет все три файла **одной** версии.
- **Update-хост ОТЛЕН от git-хоста:** `update.base_url` свой (env `UPDATE_BASE_URL`),
фоллбэка на `git.base_url` НЕТ. Пустой `update.base_url` полностью отключает обновление.
- **Токен доступа — только `read:package`, НЕ `git.token`** (least-privilege: update ≠ git).
Приоритет: вшитый ldflag `-X main.updateToken` (секрет CI `TC_UPDATE_TOKEN`) >
`update.token` из конфига (`UPDATE_TOKEN`). Заголовок: `Authorization: token <токен>`.
- **Применение (свап):** POSIX — exec-перезапуск процесса; Windows — мгновенный exec
невозможен (файл занят), поэтому `.new` применяется при следующем старте
(`ApplyPendingSwap` в main.go до подсистем). Резервный `.old` для отката.
### Как опубликовать новую версию
1. Внеси правки, коммитни, запуши в `main`.
2. CI соберёт оба бинаря со вшитым `TC_UPDATE_TOKEN` и опубликует в
`.../generic/ratatoskr/commit-<sha7>/` (бинарь + `.version` + `.sha256`).
3. На обновляемой машине: бот сам уведомит о новой версии → `/update`.
Проверку автообновления после публикации гоняет владелец на своей
Windows-машине (не прогоняется агентом).
### Валидация опубликованной версии
```bash
B=http://gitea.hal9000.home
V=commit-<sha7>; f=ratatoskr-linux-amd64
curl -s -H "Authorization: token $TOKEN" -o /tmp/probe.bin "$B/api/packages/kamelion/generic/ratatoskr/$V/$f"
echo "actual: $(sha256sum /tmp/probe.bin)" # реальная сумма скачанного
curl -s -H "Authorization: token $TOKEN" "$B/api/packages/kamelion/generic/ratatoskr/$V/$f.version"
curl -s -H "Authorization: token $TOKEN" "$B/api/packages/kamelion/generic/ratatoskr/$V/$f.sha256"
# .version == $V и .sha256 == actual → версия целостна
```
## Классы ошибок
| Блок | Коды | Где |
|---|---|---|
| C | C1C4 | `internal/config` |
| A | A1A4 | `internal/analyst` |
| M | M1M5 | `internal/chat` |
| O | O1O5 | `internal/opencode` |
| S | S1S5 | `internal/storage` |
| W | W1W5 | `internal/worker` |
| E | E1E4 | `internal/worker` (репозитории) |
| R | R1R6 | `internal/worker/review_errors.go` (ревьюер) |
| U | U1U6 | `internal/update` |
## Контракты (не ломать)
- `App.New(configPath, version, updateToken string)` — сигнатура: при правках обновлять
все вызовы в `internal/app/app_test.go` (иначе `go vet` падает).
- `packageOwner` — константа (`"kamelion"`) в `internal/app/app.go`: владелец пакета ==
владелец репо == создатель токена; инвариант на проект, НЕ плодить vars/ldflag/конфиг.
- `update.Updater` создаётся структурой `&update.Updater{...}` напрямую (конструктора
`NewUpdater` нет).
- `process_turn` (Python-версия) всегда возвращает 3 значения; `decide_fn` — кортеж
`(decision, err)`. Вердикты агентов — строгий JSON.
## Дополнительно
- Дизайн eval-suite (парсинг A / контракты B / качество C) — `../wiki/concepts/ratatoskr-evals.md`.
- Прочие команды/go-модель — см. скилл `ratatoskr-development`.