feat(config): Add multi-agent support with pluggable registry

Implements agent abstraction layer to support multiple AI coding agents.

Built-in presets (E2E tested):
- Claude Code (default)
- Gemini CLI
- OpenAI Codex

Key changes:
- Add AgentRegistry with built-in presets and custom agent support
- Add TownSettings with default_agent and custom agents map
- Add Agent field to RigSettings for per-rig agent selection
- Update ResolveAgentConfig for hierarchical config resolution
- Update spawn paths to use configured agent instead of hardcoded claude

Configuration hierarchy (first match wins):
1. Rig's Runtime config (backwards compat)
2. Rig's Agent -> custom agents -> built-in presets
3. Town's default_agent setting
4. Fallback to Claude

Additional agents (aider, opencode, etc.) can be added via config file:
settings/agents.json

Addresses Issue #10: Agent Agnostic Engine with Multi-provider support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
mayor
2026-01-04 14:46:54 -05:00
committed by Raymond Weitekamp
parent 8dea4acf8c
commit ea1a41f1f5
7 changed files with 709 additions and 20 deletions

View File

@@ -8,6 +8,7 @@ import (
"strings"
"syscall"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/constants"
"github.com/steveyegge/gastown/internal/crew"
"github.com/steveyegge/gastown/internal/git"
@@ -147,21 +148,26 @@ func isShellCommand(cmd string) bool {
return false
}
// execClaude execs claude, replacing the current process.
// Used when we're already in the target session and just need to start Claude.
// If prompt is provided, it's passed as the initial prompt to Claude.
func execClaude(prompt string) error {
claudePath, err := exec.LookPath("claude")
if err != nil {
return fmt.Errorf("claude not found: %w", err)
// execAgent execs the configured agent, replacing the current process.
// Used when we're already in the target session and just need to start the agent.
// If prompt is provided, it's passed as the initial prompt.
func execAgent(cfg *config.RuntimeConfig, prompt string) error {
if cfg == nil {
cfg = config.DefaultRuntimeConfig()
}
// exec replaces current process with claude
args := []string{"claude", "--dangerously-skip-permissions"}
agentPath, err := exec.LookPath(cfg.Command)
if err != nil {
return fmt.Errorf("%s not found: %w", cfg.Command, err)
}
// exec replaces current process with agent
// args[0] must be the command name (convention for exec)
args := append([]string{cfg.Command}, cfg.Args...)
if prompt != "" {
args = append(args, prompt)
}
return syscall.Exec(claudePath, args, os.Environ())
return syscall.Exec(agentPath, args, os.Environ())
}
// isInTmuxSession checks if we're currently inside the target tmux session.