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 обновлены
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user