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

@@ -188,7 +188,7 @@ func (d *Daemon) heartbeat(state *State) {
// 6. Trigger pending polecat spawns (bootstrap mode - ZFC violation acceptable)
// This ensures polecats get nudged even when Deacon isn't in a patrol cycle.
// Uses regex-based WaitForClaudeReady, which is acceptable for daemon bootstrap.
// Uses regex-based WaitForRuntimeReady, which is acceptable for daemon bootstrap.
d.triggerPendingSpawns()
// 7. Process lifecycle requests
@@ -498,7 +498,7 @@ func (d *Daemon) isRigOperational(rigName string) (bool, string) {
}
// triggerPendingSpawns polls pending polecat spawns and triggers those that are ready.
// This is bootstrap mode - uses regex-based WaitForClaudeReady which is acceptable
// This is bootstrap mode - uses regex-based WaitForRuntimeReady which is acceptable
// for daemon operations when no AI agent is guaranteed to be running.
// The timeout is short (2s) to avoid blocking the heartbeat.
func (d *Daemon) triggerPendingSpawns() {
@@ -517,7 +517,7 @@ func (d *Daemon) triggerPendingSpawns() {
d.logger.Printf("Found %d pending spawn(s), attempting to trigger...", len(pending))
// Trigger pending spawns (uses WaitForClaudeReady with short timeout)
// Trigger pending spawns (uses WaitForRuntimeReady with short timeout)
results, err := polecat.TriggerPendingSpawns(d.config.TownRoot, triggerTimeout)
if err != nil {
d.logger.Printf("Error triggering spawns: %v", err)

View File

@@ -466,6 +466,10 @@ func (d *Daemon) getStartCommand(roleConfig *beads.RoleConfig, parsed *ParsedIde
// Default command for all agents - use runtime config
defaultCmd := "exec " + config.GetRuntimeCommand(rigPath)
runtimeConfig := config.LoadRuntimeConfig(rigPath)
if runtimeConfig.Session != nil && runtimeConfig.Session.SessionIDEnv != "" {
defaultCmd = config.PrependEnv(defaultCmd, map[string]string{"GT_SESSION_ID_ENV": runtimeConfig.Session.SessionIDEnv})
}
// Polecats need environment variables set in the command
if parsed.RoleType == "polecat" {