feat: Add Codex and OpenCode runtime backend support (#281)

Adds support for alternative AI runtime backends (Codex, OpenCode) alongside
the default Claude backend through a runtime abstraction layer.

- internal/runtime/runtime.go - Runtime-agnostic helper functions
- Extended RuntimeConfig with provider-specific settings
- internal/opencode/ for OpenCode plugin support
- Updated session managers to use runtime abstraction
- Removed unused ensureXxxSession functions
- Fixed daemon.go indentation, updated terminology to runtime

Backward compatible: Claude remains default runtime.

Co-Authored-By: Ben Kraus <ben@cinematicsoftware.com>
Co-Authored-By: Cameron Palmer <cameronmpalmer@users.noreply.github.com>
This commit is contained in:
george
2026-01-08 22:56:37 -08:00
committed by Steve Yegge
33 changed files with 850 additions and 176 deletions

View File

@@ -9,6 +9,7 @@ import (
"strings"
"time"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/mail"
"github.com/steveyegge/gastown/internal/tmux"
)
@@ -195,15 +196,17 @@ func TriggerPendingSpawns(townRoot string, timeout time.Duration) ([]TriggerResu
continue
}
// Check if Claude is ready (non-blocking poll)
err = t.WaitForClaudeReady(ps.Session, timeout)
// Check if runtime is ready (non-blocking poll)
rigPath := filepath.Join(townRoot, ps.Rig)
runtimeConfig := config.LoadRuntimeConfig(rigPath)
err = t.WaitForRuntimeReady(ps.Session, runtimeConfig, timeout)
if err != nil {
// Not ready yet - keep in pending
remaining = append(remaining, ps)
continue
}
// Claude is ready - send trigger
// Runtime is ready - send trigger
triggerMsg := "Begin."
if err := t.NudgeSession(ps.Session, triggerMsg); err != nil {
result.Error = fmt.Errorf("nudging session: %w", err)

View File

@@ -10,10 +10,10 @@ import (
"strings"
"time"
"github.com/steveyegge/gastown/internal/claude"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/constants"
"github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/runtime"
"github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/tmux"
)
@@ -59,9 +59,9 @@ type SessionStartOptions struct {
// Account specifies the account handle to use (overrides default).
Account string
// ClaudeConfigDir is resolved CLAUDE_CONFIG_DIR for the account.
// RuntimeConfigDir is resolved config directory for the runtime account.
// If set, this is injected as an environment variable.
ClaudeConfigDir string
RuntimeConfigDir string
}
// SessionInfo contains information about a running polecat session.
@@ -134,11 +134,13 @@ func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error {
workDir = m.polecatDir(polecat)
}
// Ensure Claude settings exist in polecats/ (not polecats/<name>/) so we don't
// write into the source repo. Claude walks up the tree to find settings.
runtimeConfig := config.LoadRuntimeConfig(m.rig.Path)
// Ensure runtime settings exist in polecats/ (not polecats/<name>/) so we don't
// write into the source repo. Runtime walks up the tree to find settings.
polecatsDir := filepath.Join(m.rig.Path, "polecats")
if err := claude.EnsureSettingsForRole(polecatsDir, "polecat"); err != nil {
return fmt.Errorf("ensuring Claude settings: %w", err)
if err := runtime.EnsureSettingsForRole(polecatsDir, "polecat", runtimeConfig); err != nil {
return fmt.Errorf("ensuring runtime settings: %w", err)
}
// Create session
@@ -150,9 +152,9 @@ func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error {
debugSession("SetEnvironment GT_RIG", m.tmux.SetEnvironment(sessionID, "GT_RIG", m.rig.Name))
debugSession("SetEnvironment GT_POLECAT", m.tmux.SetEnvironment(sessionID, "GT_POLECAT", polecat))
// Set CLAUDE_CONFIG_DIR for account selection (non-fatal)
if opts.ClaudeConfigDir != "" {
debugSession("SetEnvironment CLAUDE_CONFIG_DIR", m.tmux.SetEnvironment(sessionID, "CLAUDE_CONFIG_DIR", opts.ClaudeConfigDir))
// Set runtime config dir for account selection (non-fatal)
if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && opts.RuntimeConfigDir != "" {
debugSession("SetEnvironment "+runtimeConfig.Session.ConfigDirEnv, m.tmux.SetEnvironment(sessionID, runtimeConfig.Session.ConfigDirEnv, opts.RuntimeConfigDir))
}
// Set beads environment for worktree polecats (non-fatal)
@@ -183,6 +185,10 @@ func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error {
if command == "" {
command = config.BuildPolecatStartupCommand(m.rig.Name, polecat, m.rig.Path, "")
}
// Prepend runtime config dir env if needed
if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && opts.RuntimeConfigDir != "" {
command = config.PrependEnv(command, map[string]string{runtimeConfig.Session.ConfigDirEnv: opts.RuntimeConfigDir})
}
// Wait for shell to be ready before sending keys (prevents "can't find pane" under load)
if err := m.tmux.WaitForShellReady(sessionID, 5*time.Second); err != nil {
_ = m.tmux.KillSession(sessionID)
@@ -198,8 +204,9 @@ func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error {
// Accept bypass permissions warning dialog if it appears
debugSession("AcceptBypassPermissionsWarning", m.tmux.AcceptBypassPermissionsWarning(sessionID))
// Wait for Claude to be fully ready
time.Sleep(8 * time.Second)
// Wait for runtime to be fully ready at the prompt (not just started)
runtime.SleepForReadyDelay(runtimeConfig)
_ = runtime.RunStartupFallback(m.tmux, sessionID, "polecat", runtimeConfig)
// Inject startup nudge for predecessor discovery via /resume
address := fmt.Sprintf("%s/polecats/%s", m.rig.Name, polecat)