From f0794300b57af8e3443bc7b715241910729e770f Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 16 Aug 2026 21:43:06 +0500 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=BF=D0=BE=D0=B4=D1=80=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D1=81=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=D0=BA=D0=B5=20=D0=B8=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8E=20=D0=B8=D0=B7?= =?UTF-8?q?=20Gitea=20Packages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 186 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 05ad0fb..528ba12 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,205 @@ # Ratatoskr-go Ratatoskr на Go — **оркестратор в единый статический бинарь** + субагенты через -[opencode](https://opencode.ai) как внешний процесс. Переезд с Python-версии. - -## Почему Go - -Агентный конвейер — это клей (HTTP/JSON к LLM, SQLite, Gitea API, Telegram), а не -горячие вычисления. Цель — **один статический бинарь без runtime-зависимостей** -(CGO_ENABLED=0), который решает проблему доставки/деплоя. Go: горизонт -HTTP/JSON + goroutine-параллелизм research/feedback/notify + SQLite без CGO -(`modernc.org/sqlite`). +[opencode](https://opencode.ai) как внешний процесс. Переезд с Python-версии: +все подсистемы (диалог, воркер, автообновление) собраны в один исполняемый +файл без runtime-зависимостей (`CGO_ENABLED=0`), что упрощает доставку и деплой. ## Структура ``` -cmd/ratatoskr/ # точка входа, сборка бинаря +cmd/ratatoskr/ # точка входа, сборка бинаря (App.New + Run) internal/ - app/ # компоновка (composition root), конфиг, DI - dialog/ # state-machine диалога: collect→confirm→created - decide/ # аналитик: промпт + разбор JSON-решения (opencode agent) - research/ # исследователь: FACTS о репозитории - feedback/ # feedback: транскрипт→proposals (opencode agent) - opencode/ # обёртка запуска opencode-процесса, парсинг вердикта - gitea/ # Gitea API-клиент: issues, labels, PR, webhook - webhook/ # digest-ответы и статус в Telegram - issue/ # issue: формирование тела задачи, task-tag + app/ # компоновка (composition root), config, DI, packageOwner + config/ # загрузка YAML+env: ε подстановка ${VAR:-default}, defaults, validate (классы C1–C4) + chat/ # мультиканальный Router; telegram — long-poll канал + core/ # state-machine задач: /start /cancel /skip /retry N /status N /continue N + analyst/ # аналитик: промпт + разбор JSON-решения (opencode agent, классы A1–A4) + worker/ # polling-планировщик + dev/reviewer-конвейер (классы W*, E*, R*) + agents/ # встроенные агенты opencode (analyst.md, dev.md, reviewer.md) через go:embed + opencode/ # обёртка запуска opencode-процесса, парсинг вердикта (классы O1–O4) + storage/ # SQLite (modernc.org/sqlite, без CGO): tasks, traces, task_history (S1–S5) + update/ # автообновление из Gitea Packages (классы U1–U6) ``` ## Сборка +Требуется Go (на хосте без `go` в PATH — экспорт вручную): + ```bash -go build -o bin/ratatoskr ./cmd/ratatoskr # обычная -CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ - -o bin/ratatoskr-linux-amd64 ./cmd/ratatoskr # статический бинарь +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-` — та же версия, под которой бинарь публикуется в 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-/` + (псевдо-версия `latest` НЕ используется — см. автообновление). Плюс companion-файлы + `.version` и `.sha256`. + +Секреты/vars: `TC_GITEA_TOKEN` (write:packages), `TC_UPDATE_TOKEN` (read:package), +`GIT_MAIN_URL`. + +## Конфигурация (config.yaml) + +Минимальный рабочий конфиг (порядок полей произвольный) и блок автообновления +(подробнее в `config.yaml.example`): + +```yaml +telegram: + token: "..." # TG_TOKEN + chat_id: "..." # TG_CHAT_ID +opencode: + bin: "opencode" + config_dir: "./agents" # каталог, куда распаковываются встроенные агенты +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` | справка по всем командам | + +## Автообновление из Gitea Packages + +Бинарь умеет сам себя обновлять из generic-пакета в Gitea. Модель: +**авто = только Check + уведомление** владельцу («доступна версия X, выполни /update»), +**применение — только по `/update`**. + +- **Источник версий:** `{update.base_url}/api/packages/{owner}/generic/{package}`, + где `owner = "kamelion"` (константа `packageOwner` в коде), `package = "ratatoskr"`. +- **Каждая публикация — в конкретную версию `commit-/`** (никакого `latest/`: + при параллельных PUT матрицы CI псевдо-версия `latest` разъезжается, и контрольные + суммы не сходятся — класс ошибки U4). Новейшая применимая версия ищется листингом: + фильтр `commit-*`, проба скачать бинарь платформы, берём версию с макс `id`. +- **Companion-файлы** рядом с бинарём в той же версии: `..version` (id версии) + и `.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-/` (бинарь + `.version` + `.sha256`). +3. На обновляемой машине: бот сам уведомит о новой версии → `/update`. + +Проверку автообновления после публикации гоняет владелец на своей +Windows-машине (не прогоняется агентом). + +### Валидация опубликованной версии + +```bash +B=http://gitea.hal9000.home +V=commit-; 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 | C1–C4 | `internal/config` | +| A | A1–A4 | `internal/analyst` | +| M | M1–M5 | `internal/chat` | +| O | O1–O4 | `internal/opencode` | +| S | S1–S5 | `internal/storage` | +| W | W1–W5 | `internal/worker` | +| E | E1–E4 | `internal/worker` (репозитории) | +| R | R1–R6 | `internal/worker/review_errors.go` (ревьюер) | +| U | U1–U6 | `internal/update` | + ## Контракты (не ломать) -- `process_turn` всегда возвращает **3 значения**: `(reply, action, session)`. -- `decide_fn` возвращает кортеж/структуру `(decision, err)`. -- `/proposal` передаёт аналитику **полный комплект**, а не digest. -- Вердикты агентов — строгий JSON; `_extract_json` устойчив к мусору. +- `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 +## Дополнительно -Eval-suite (парсинг A / контракты B / качество C) — см. -[дизайн в вики](../wiki/concepts/ratatoskr-evals.md). Запуск после внедрения: -`go test ./...` для детерминированных уровней A+B. \ No newline at end of file +- Дизайн eval-suite (парсинг A / контракты B / качество C) — `../wiki/concepts/ratatoskr-evals.md`. +- Прочие команды/go-модель — см. скилл `ratatoskr-development`. \ No newline at end of file