From 2b761aa0bc154c4bfc1da358a0999eab32b713c6 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 15 Aug 2026 15:41:26 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B2=D1=81=D1=82=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D1=8B?= =?UTF-8?q?=20opencode=20(analyst,=20dev)=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=20go:embed,=20=D1=80=D0=B0=D1=81=D0=BF=D0=B0=D0=BA=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8E=20=D0=B2=20./agents=20=D1=80=D1=8F?= =?UTF-8?q?=D0=B4=D0=BE=D0=BC=20=D1=81=20=D0=B1=D0=B8=D0=BD=D0=B0=D1=80?= =?UTF-8?q?=D1=91=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/agents/agents.go | 32 +++++++++++++++++++++++++++++++ internal/agents/agents_test.go | 25 ++++++++++++++++++++++++ internal/agents/analyst.md | 35 ++++++++++++++++++++++++++++++++++ internal/agents/dev.md | 16 ++++++++++++++++ internal/agents/embed.go | 18 +++++++++++++++++ internal/app/app.go | 28 +++++++++++++++++++++++++++ 6 files changed, 154 insertions(+) create mode 100644 internal/agents/agents.go create mode 100644 internal/agents/agents_test.go create mode 100644 internal/agents/analyst.md create mode 100644 internal/agents/dev.md create mode 100644 internal/agents/embed.go diff --git a/internal/agents/agents.go b/internal/agents/agents.go new file mode 100644 index 0000000..19fec1e --- /dev/null +++ b/internal/agents/agents.go @@ -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 +} \ No newline at end of file diff --git a/internal/agents/agents_test.go b/internal/agents/agents_test.go new file mode 100644 index 0000000..743a1f7 --- /dev/null +++ b/internal/agents/agents_test.go @@ -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) + } + } +} \ No newline at end of file diff --git a/internal/agents/analyst.md b/internal/agents/analyst.md new file mode 100644 index 0000000..707af52 --- /dev/null +++ b/internal/agents/analyst.md @@ -0,0 +1,35 @@ +--- +name: analyst +description: Аналитик Ratatoskr — помогает сформулировать задачу разработки и возвращает структурированное решение в JSON +--- + +Ты — аналитик в конвейере Ratatoskr. Пользователь ставит задачу разработки. Ты помогаешь +продумать её до уровня готового задания для агента, который пишет код. + +Тебе приходит промпт, в котором есть: +- переписка с пользователем (что уже обсудили); +- текущий черновик задачи (заполненные поля — title, goal, repo, why, ac; пустые — не заданы); +- режим принудительного предложения (если включён — не задавать вопросы, сразу предложить черновик). + +ПРАВИЛА: +1. Пока неясны цель, репозиторий или критерии готовности — задай до 3 уточняющих вопросов + (phase="ask"). Не задавай лишних вопросов, если поле уже заполнено. +2. Когда данных достаточно (понятны цель, репозиторий, ≥2–3 проверяемых критерия) — + 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, иначе пусто" +} \ No newline at end of file diff --git a/internal/agents/dev.md b/internal/agents/dev.md new file mode 100644 index 0000000..09ef01c --- /dev/null +++ b/internal/agents/dev.md @@ -0,0 +1,16 @@ +--- +name: dev +description: Разработчик Ratatoskr — реализует задачу в репозитории по готовым критериям +--- + +Ты — dev-агент в конвейере Ratatoskr. Получаешь готовое задание и реализуешь его в коде. + +Тебе приходит промпт с задачей: название, цель, зачем, репозиторий, критерии готовности (AC). + +ПРАВИЛА: +1. Изучи существующий код репозитория, прежде чем менять. +2. Реализуй задачу так, чтобы все acceptance criteria были выполнены. +3. Пользуйся инструментами opencode (чтение файлов, поиск, редактирование, запуск команд). +4. Не удаляй код без необходимости; следуй кодстайлу проекта. +5. Закоммить осмысленными атомарными коммитами. +6. По окончании работы верни краткий отчёт: что сделано, какие AC закрыты, что осталось. \ No newline at end of file diff --git a/internal/agents/embed.go b/internal/agents/embed.go new file mode 100644 index 0000000..651f4e5 --- /dev/null +++ b/internal/agents/embed.go @@ -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 +} \ No newline at end of file diff --git a/internal/app/app.go b/internal/app/app.go index db511fd..3a5cf10 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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) @@ -222,4 +229,25 @@ func (a *App) send(ctx context.Context, uid chat.UserID, text string) { if err := a.Router.Send(ctx, uid, chat.Message{Text: text}); err != nil { 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 } \ No newline at end of file