fix(opencode): агенты — в <worktree>/.opencode/agent, OPENCODE_CONFIG_DIR больше не выставляем
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

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 обновлены
This commit is contained in:
ki.sagidullin
2026-08-23 17:54:35 +05:00
parent d731a7429d
commit 8c91c83024
10 changed files with 44 additions and 78 deletions

View File

@@ -1,12 +1,10 @@
// Package agents — встроенные агенты opencode (analyst, dev, ...).
//
// Агенты зашиты в бинарь через go:embed и распаковываются при запуске
// в каталог OPENCODE_CONFIG_DIR (по умолчанию ./agents рядом с бинарём),
// чтобы opencode находил их независимо от того, где запущен ratatoskr.
//
// opencode сканирует OPENCODE_CONFIG_DIR как каталог .opencode: агенты
// ищутся в поддиректории agents/ (мн.ч.), а не в корне. Поэтому распаковка
// идёт в <dir>/agents/*.md.
// в project-каталог <worktree>/.opencode/agent/*.md. opencode при serve
// (cwd = worktree) обнаруживает .opencode поднятием вверх (ConfigPaths) и
// находит агентов по шаблону {agent,agents}/**/*.md. Глобальный конфиг
// opencode остаётся нетронутым (OPENCODE_CONFIG_DIR не выставляем).
package agents
import (
@@ -23,10 +21,10 @@ var FS embed.FS
// в FS, но для детерминированной распаковки список лучше дополнять здесь.
var Names = []string{"analyst", "dev", "reviewer", "chat", "postmortem"}
// WriteTo распаковывает всех встроенных агентов в каталог dir/agents
// WriteTo распаковывает всех встроенных агентов в каталог dir/agent
// (создаёт его). Файлы перезаписываются — встроенная копия всегда актуальна.
func WriteTo(dir string) error {
agentsDir := filepath.Join(dir, "agents")
agentsDir := filepath.Join(dir, "agent")
for _, name := range Names {
data, err := FS.ReadFile(name + ".md")
if err != nil {

View File

@@ -13,8 +13,8 @@ func TestWriteTo(t *testing.T) {
t.Fatalf("WriteTo: %v", err)
}
for _, name := range Names {
// opencode ищет агентов в поддиректории agents/, как у .opencode
p := filepath.Join(dir, "agents", name+".md")
// opencode ищет агентов в поддиректории agent/ (или agents/) внутри .opencode
p := filepath.Join(dir, "agent", name+".md")
data, err := os.ReadFile(p)
if err != nil {
t.Fatalf("read %s: %v", p, err)

View File

@@ -157,7 +157,6 @@ func New(configPath, version, updateToken string, noUI bool) (*App, error) {
ocPool := opencode.NewPool(cfg.Paths.Worktree)
ocPool.Bin = cfg.OpenCode.Bin
ocPool.Config = cfg.OpenCode.Config
ocPool.ConfigDir = cfg.OpenCode.ConfigDir
ocPool.DBPath = cfg.OpenCode.DBPath
ocPool.Host = cfg.OpenCode.Serve.Hostname
ocPool.BasePort = cfg.OpenCode.Serve.Port
@@ -665,23 +664,21 @@ func (a *App) handleUpdateCmd(ctx context.Context, uid chat.UserID) {
// сюда не возвращаемся — SwapAndRestart завершил процесс (os.Exit)
}
// ensureAgentsDir определяет каталог с агентами opencode и распаковывает
// туда встроенных агентов (go:embed). Если config_dir не задан — использует
// ./agents рядом с бинарём. Встроенные агенты перезаписываются (всегда актуальны).
// ensureAgentsDir распаковывает встроенных агентов opencode (go:embed) в
// project-каталог <worktree>/.opencode/agent/*.md. opencode при serve (cwd =
// worktree) обнаруживает .opencode поднятием вверх (paths.ts) и сам находит
// агентов. Глобальный конфиг opencode (модель/провайдеры) не трогаем и
// OPENCODE_CONFIG_DIR не выставляем.
//
// 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 cfg.Paths.Worktree == "" {
return fmt.Errorf("paths.worktree не задан — некуда раскладывать агентов")
}
if err := agents.WriteTo(cfg.OpenCode.ConfigDir); err != nil {
return fmt.Errorf("write agents to %s: %w", cfg.OpenCode.ConfigDir, err)
base := filepath.Join(cfg.Paths.Worktree, ".opencode")
if err := agents.WriteTo(base); err != nil {
return fmt.Errorf("write agents to %s: %w", base, err)
}
log.Printf("app: agents ensured in %s", cfg.OpenCode.ConfigDir)
log.Printf("app: agents ensured in %s", base)
return nil
}

View File

@@ -146,34 +146,6 @@ log:
}
}
func TestLoad_OpenCodeConfigDir(t *testing.T) {
t.Setenv("TG_TOKEN", "tok")
t.Setenv("TG_CHAT_ID", "42")
// без config_dir — пусто
yaml := `telegram:
token: "${TG_TOKEN}"
chat_id: "${TG_CHAT_ID}"
`
cfg, err := Load(writeCfg(t, yaml))
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.OpenCode.ConfigDir != "" {
t.Errorf("config_dir = %q, want пусто", cfg.OpenCode.ConfigDir)
}
// через env OPENCODE_CONFIG_DIR
t.Setenv("OPENCODE_CONFIG_DIR", "/opt/agents")
cfg, err = Load(writeCfg(t, yaml))
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.OpenCode.ConfigDir != "/opt/agents" {
t.Errorf("config_dir = %q, want /opt/agents", cfg.OpenCode.ConfigDir)
}
}
func TestLoad_EnvOverride(t *testing.T) {
t.Setenv("TG_TOKEN", "tok")
t.Setenv("TG_CHAT_ID", "42")

View File

@@ -89,7 +89,6 @@ type OpenCodeCfg struct {
Bin string `yaml:"bin" default:"opencode"`
DBPath string `yaml:"db_path" default:""`
Config string `yaml:"config" env:"OPENCODE_CONFIG"`
ConfigDir string `yaml:"config_dir" env:"OPENCODE_CONFIG_DIR"`
HardTimeout Duration `yaml:"hard_timeout" default:"20m"`
IdleTimeout Duration `yaml:"idle_timeout" default:"5m"`
PollMs Duration `yaml:"poll_ms" default:"2s"`

View File

@@ -20,7 +20,6 @@ import (
type Pool struct {
Bin string
Config string
ConfigDir string
DBPath string
Host string
BasePort int
@@ -72,13 +71,12 @@ func (p *Pool) EnsureRoot(ctx context.Context) error {
return nil
}
s := &Server{
Bin: p.Bin,
Config: p.Config,
ConfigDir: p.ConfigDir,
DBPath: p.DBPath,
Host: p.Host,
Password: p.Password,
Dir: p.rootDir,
Bin: p.Bin,
Config: p.Config,
DBPath: p.DBPath,
Host: p.Host,
Password: p.Password,
Dir: p.rootDir,
}
if err := p.assign(s); err != nil {
return err
@@ -102,13 +100,12 @@ func (p *Pool) Ensure(ctx context.Context, dir string) (*Server, error) {
}
abs := filepath.Clean(dir)
s := &Server{
Bin: p.Bin,
Config: p.Config,
ConfigDir: p.ConfigDir,
DBPath: p.DBPath,
Host: p.Host,
Password: p.Password,
Dir: abs,
Bin: p.Bin,
Config: p.Config,
DBPath: p.DBPath,
Host: p.Host,
Password: p.Password,
Dir: abs,
}
if err := p.assign(s); err != nil {
p.mu.Unlock()

View File

@@ -25,7 +25,6 @@ import (
type Server struct {
Bin string // путь к opencode (по умолчанию "opencode")
Config string // OPENCODE_CONFIG
ConfigDir string // OPENCODE_CONFIG_DIR
DBPath string // рабочая БД сервера (передам env, если задана)
Host string // hostname для прослушивания
@@ -127,12 +126,13 @@ func (s *Server) serveCmd(ctx context.Context) *exec.Cmd {
env := append(os.Environ(),
"OPENCODE_DISABLE_AUTOUPDATE=1",
"OPENCODE_DISABLE_MODELS_FETCH=1")
// Агенты (analyst/dev/...) opencode находит сам через project-каталог
// .opencode (см. internal/agents); OPENCODE_CONFIG_DIR не выставляем,
// чтобы не перенаправлять Global.Path.config и сохранить глобальный
// конфиг opencode (модель/провайдеры).
if s.Config != "" {
env = append(env, "OPENCODE_CONFIG="+s.Config)
}
if s.ConfigDir != "" {
env = append(env, "OPENCODE_CONFIG_DIR="+s.ConfigDir)
}
if s.DBPath != "" {
env = append(env, "OPENCODE_DB="+s.DBPath)
}