feat: встроенные агенты opencode (analyst, dev) через go:embed, распаковка по умолчанию в ./agents рядом с бинарём
All checks were successful
CI / test (push) Successful in 38s
CI / build-and-package (amd64, darwin) (push) Successful in 34s
CI / build-and-package (amd64, linux) (push) Successful in 33s
CI / build-and-package (amd64, windows) (push) Successful in 35s
CI / build-and-package (arm64, darwin) (push) Successful in 35s
CI / build-and-package (arm64, linux) (push) Successful in 34s

This commit is contained in:
Hermes
2026-08-15 15:41:26 +05:00
parent 9e7c95e1d8
commit 2b761aa0bc
6 changed files with 154 additions and 0 deletions

32
internal/agents/agents.go Normal file
View File

@@ -0,0 +1,32 @@
// Package agents — встроенные агенты opencode (analyst, dev, ...).
//
// Агенты зашиты в бинарь через go:embed и распаковываются при запуске
// в каталог OPENCODE_CONFIG_DIR (по умолчанию ./agents рядом с бинарём),
// чтобы opencode находил их независимо от того, где запущен ratatoskr.
package agents
import "embed"
//go:embed *.md
var FS embed.FS
// Names — имена распаковываемых агентов (файлы *.md, без .md).
//
// Держим в курсе: если добавить файл в каталог, он автоматически попадёт
// в FS, но для детерминированной распаковки список лучше дополнять здесь.
var Names = []string{"analyst", "dev"}
// WriteTo распаковывает всех встроенных агентов в каталог dir (создаёт его).
// Файлы перезаписываются — встроенная копия всегда актуальна.
func WriteTo(dir string) error {
for _, name := range Names {
data, err := FS.ReadFile(name + ".md")
if err != nil {
return err
}
if err := writeFile(dir, name+".md", data); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,25 @@
package agents
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestWriteTo(t *testing.T) {
dir := t.TempDir()
if err := WriteTo(dir); err != nil {
t.Fatalf("WriteTo: %v", err)
}
for _, name := range Names {
p := filepath.Join(dir, name+".md")
data, err := os.ReadFile(p)
if err != nil {
t.Fatalf("read %s: %v", p, err)
}
if !strings.Contains(string(data), "name: "+name) {
t.Errorf("%s: нет frontmatter name: %s", p, name)
}
}
}

View File

@@ -0,0 +1,35 @@
---
name: analyst
description: Аналитик Ratatoskr — помогает сформулировать задачу разработки и возвращает структурированное решение в JSON
---
Ты — аналитик в конвейере Ratatoskr. Пользователь ставит задачу разработки. Ты помогаешь
продумать её до уровня готового задания для агента, который пишет код.
Тебе приходит промпт, в котором есть:
- переписка с пользователем (что уже обсудили);
- текущий черновик задачи (заполненные поля — title, goal, repo, why, ac; пустые — не заданы);
- режим принудительного предложения (если включён — не задавать вопросы, сразу предложить черновик).
ПРАВИЛА:
1. Пока неясны цель, репозиторий или критерии готовности — задай до 3 уточняющих вопросов
(phase="ask"). Не задавай лишних вопросов, если поле уже заполнено.
2. Когда данных достаточно (понятны цель, репозиторий, ≥23 проверяемых критерия) —
phase="propose" и в chat_reply краткое резюме черновика.
3. Если тема явно не про разработку кода или не подходит для агента-кодера — phase="abort"
с причиной.
4. Возвращай ВСЕГДА строго один JSON-объект без markdown-обрамления и без лишнего текста.
5. Меняй в JSON только те поля, которые надо изменить; незаполненные — пустой строкой.
Формат ответа:
{
"phase": "ask|propose|abort",
"title": "",
"goal": "",
"repo": "",
"why": "",
"ac": "",
"questions": ["вопрос 1", "вопрос 2"],
"chat_reply": "твой ответ пользователю (на русском, естественно)",
"abort_reason": "причина, если phase=abort, иначе пусто"
}

16
internal/agents/dev.md Normal file
View File

@@ -0,0 +1,16 @@
---
name: dev
description: Разработчик Ratatoskr — реализует задачу в репозитории по готовым критериям
---
Ты — dev-агент в конвейере Ratatoskr. Получаешь готовое задание и реализуешь его в коде.
Тебе приходит промпт с задачей: название, цель, зачем, репозиторий, критерии готовности (AC).
ПРАВИЛА:
1. Изучи существующий код репозитория, прежде чем менять.
2. Реализуй задачу так, чтобы все acceptance criteria были выполнены.
3. Пользуйся инструментами opencode (чтение файлов, поиск, редактирование, запуск команд).
4. Не удаляй код без необходимости; следуй кодстайлу проекта.
5. Закоммить осмысленными атомарными коммитами.
6. По окончании работы верни краткий отчёт: что сделано, какие AC закрыты, что осталось.

18
internal/agents/embed.go Normal file
View File

@@ -0,0 +1,18 @@
package agents
// WriteTo распаковывает всех встроенных агентов в каталог dir.
import (
"os"
"path/filepath"
)
func writeFile(dir, name string, data []byte) error {
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
p := filepath.Join(dir, name)
if err := os.WriteFile(p, data, 0o644); err != nil {
return err
}
return nil
}

View File

@@ -8,10 +8,12 @@ import (
"log"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/kamelion/ratatoskr-go/internal/agents"
"github.com/kamelion/ratatoskr-go/internal/analyst"
"github.com/kamelion/ratatoskr-go/internal/chat"
"github.com/kamelion/ratatoskr-go/internal/chat/telegram"
@@ -46,6 +48,11 @@ func New(configPath string) (*App, error) {
// SQLite для задач и трасс
ctx := context.Background()
// Агенты opencode: если config_dir не задан — используем ./agents рядом с бинарём
if err := ensureAgentsDir(cfg); err != nil {
return nil, fmt.Errorf("%w: %v", ErrConfig, err)
}
store, err := storage.Open(ctx, cfg.Paths.DB)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrDBOpen, err)
@@ -223,3 +230,24 @@ func (a *App) send(ctx context.Context, uid chat.UserID, text string) {
log.Printf("app: send to %s: %v", uid, err)
}
}
// ensureAgentsDir определяет каталог с агентами opencode и распаковывает
// туда встроенных агентов (go:embed). Если config_dir не задан — использует
// ./agents рядом с бинарём. Встроенные агенты перезаписываются (всегда актуальны).
//
// idempotent: вызывается только из New.
func ensureAgentsDir(cfg *config.Config) error {
if cfg.OpenCode.ConfigDir == "" {
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("resolve executable: %w", err)
}
// ./agents рядом с бинарём
cfg.OpenCode.ConfigDir = filepath.Join(filepath.Dir(exe), "agents")
}
if err := agents.WriteTo(cfg.OpenCode.ConfigDir); err != nil {
return fmt.Errorf("write agents to %s: %w", cfg.OpenCode.ConfigDir, err)
}
log.Printf("app: agents ensured in %s", cfg.OpenCode.ConfigDir)
return nil
}