cli/pkg/agentci/config.go
Snider b72ac61698 fix(agentci): use log.E() error pattern, add Charm SSH TODOs
Replace fmt.Errorf() with structured log.E() errors in agentci, forge,
jobrunner packages. Update PipelineSignal comment to reflect dispatch
fields. Add TODO markers for charmbracelet/ssh migration across all
exec ssh call sites.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-09 11:15:11 +00:00

100 lines
3 KiB
Go

// Package agentci provides configuration and management for AgentCI dispatch targets.
package agentci
import (
"fmt"
"github.com/host-uk/core/pkg/config"
"github.com/host-uk/core/pkg/jobrunner/handlers"
"github.com/host-uk/core/pkg/log"
)
// AgentConfig represents a single agent machine in the config file.
type AgentConfig struct {
Host string `yaml:"host" mapstructure:"host"`
QueueDir string `yaml:"queue_dir" mapstructure:"queue_dir"`
ForgejoUser string `yaml:"forgejo_user" mapstructure:"forgejo_user"`
Model string `yaml:"model" mapstructure:"model"` // claude model: sonnet, haiku, opus (default: sonnet)
Runner string `yaml:"runner" mapstructure:"runner"` // runner binary: claude, codex (default: claude)
Active bool `yaml:"active" mapstructure:"active"`
}
// LoadAgents reads agent targets from config and returns a map suitable for the dispatch handler.
// Returns an empty map (not an error) if no agents are configured.
func LoadAgents(cfg *config.Config) (map[string]handlers.AgentTarget, error) {
var agents map[string]AgentConfig
if err := cfg.Get("agentci.agents", &agents); err != nil {
// No config is fine — just no agents.
return map[string]handlers.AgentTarget{}, nil
}
targets := make(map[string]handlers.AgentTarget)
for name, ac := range agents {
if !ac.Active {
continue
}
if ac.Host == "" {
return nil, log.E("agentci.LoadAgents", fmt.Sprintf("agent %q: host is required", name), nil)
}
queueDir := ac.QueueDir
if queueDir == "" {
queueDir = "/home/claude/ai-work/queue"
}
model := ac.Model
if model == "" {
model = "sonnet"
}
runner := ac.Runner
if runner == "" {
runner = "claude"
}
targets[name] = handlers.AgentTarget{
Host: ac.Host,
QueueDir: queueDir,
Model: model,
Runner: runner,
}
}
return targets, nil
}
// SaveAgent writes an agent config entry to the config file.
func SaveAgent(cfg *config.Config, name string, ac AgentConfig) error {
key := fmt.Sprintf("agentci.agents.%s", name)
data := map[string]any{
"host": ac.Host,
"queue_dir": ac.QueueDir,
"forgejo_user": ac.ForgejoUser,
"active": ac.Active,
}
if ac.Model != "" {
data["model"] = ac.Model
}
if ac.Runner != "" {
data["runner"] = ac.Runner
}
return cfg.Set(key, data)
}
// RemoveAgent removes an agent from the config file.
func RemoveAgent(cfg *config.Config, name string) error {
var agents map[string]AgentConfig
if err := cfg.Get("agentci.agents", &agents); err != nil {
return log.E("agentci.RemoveAgent", "no agents configured", err)
}
if _, ok := agents[name]; !ok {
return log.E("agentci.RemoveAgent", fmt.Sprintf("agent %q not found", name), nil)
}
delete(agents, name)
return cfg.Set("agentci.agents", agents)
}
// ListAgents returns all configured agents (active and inactive).
func ListAgents(cfg *config.Config) (map[string]AgentConfig, error) {
var agents map[string]AgentConfig
if err := cfg.Get("agentci.agents", &agents); err != nil {
return map[string]AgentConfig{}, nil
}
return agents, nil
}