From 626a24e01342faa62febc257a19bb149dadbc493 Mon Sep 17 00:00:00 2001 From: gastown/crew/gus Date: Tue, 30 Dec 2025 23:48:16 -0800 Subject: [PATCH] Refactor startup paths to use RuntimeConfig (gt-j0546) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced all hardcoded 'claude --dangerously-skip-permissions' invocations with configurable helpers from internal/config: - GetRuntimeCommand(rigPath) - simple command string - GetRuntimeCommandWithPrompt(rigPath, prompt) - with initial prompt - BuildAgentStartupCommand(role, bdActor, rigPath, prompt) - generic agent - BuildPolecatStartupCommand(rigName, polecatName, rigPath, prompt) - polecat - BuildCrewStartupCommand(rigName, crewName, rigPath, prompt) - crew - BuildStartupCommand(envVars, rigPath, prompt) - custom env vars Files updated: - internal/cmd/start.go (4 locations) - internal/cmd/crew_lifecycle.go (2 locations) - internal/cmd/crew_at.go (2 locations) - internal/cmd/deacon.go - internal/cmd/witness.go - internal/cmd/up.go (2 locations) - internal/cmd/handoff.go (2 locations) - internal/daemon/daemon.go (3 locations) - internal/daemon/lifecycle.go - internal/session/manager.go - internal/refinery/manager.go - internal/boot/boot.go This enables future support for alternative LLM runtimes (aider, etc.) via rig/town settings configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/boot/boot.go | 3 ++- internal/cmd/crew_at.go | 6 ++---- internal/cmd/crew_lifecycle.go | 7 +++---- internal/cmd/deacon.go | 3 ++- internal/cmd/handoff.go | 6 ++++-- internal/cmd/start.go | 9 +++++---- internal/cmd/up.go | 5 +++-- internal/cmd/witness.go | 3 ++- internal/daemon/daemon.go | 15 ++++++++++----- internal/daemon/lifecycle.go | 14 ++++++-------- internal/refinery/manager.go | 2 +- internal/session/manager.go | 5 ++--- 12 files changed, 42 insertions(+), 36 deletions(-) diff --git a/internal/boot/boot.go b/internal/boot/boot.go index 95262c8b..9476405b 100644 --- a/internal/boot/boot.go +++ b/internal/boot/boot.go @@ -11,6 +11,7 @@ import ( "path/filepath" "time" + "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/tmux" ) @@ -191,7 +192,7 @@ func (b *Boot) spawnTmux() error { // Launch Claude with environment exported inline and initial triage prompt // The "gt boot triage" prompt tells Boot to immediately start triage (GUPP principle) - startCmd := `export GT_ROLE=boot BD_ACTOR=deacon-boot && claude --dangerously-skip-permissions "gt boot triage"` + startCmd := config.BuildAgentStartupCommand("boot", "deacon-boot", "", "gt boot triage") if err := b.tmux.SendKeys(SessionName, startCmd); err != nil { return fmt.Errorf("sending startup command: %w", err) } diff --git a/internal/cmd/crew_at.go b/internal/cmd/crew_at.go index 8310e822..9a48337e 100644 --- a/internal/cmd/crew_at.go +++ b/internal/cmd/crew_at.go @@ -150,8 +150,7 @@ func runCrewAt(cmd *cobra.Command, args []string) error { // This gives cleaner lifecycle: Claude exits → session ends (no intermediate shell) // Pass "gt prime" as initial prompt so Claude loads context immediately // Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes - bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name) - claudeCmd := fmt.Sprintf(`export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions "gt prime"`, r.Name, name, bdActor, bdActor) + claudeCmd := config.BuildCrewStartupCommand(r.Name, name, r.Path, "gt prime") if err := t.RespawnPane(paneID, claudeCmd); err != nil { return fmt.Errorf("starting claude: %w", err) } @@ -175,8 +174,7 @@ func runCrewAt(cmd *cobra.Command, args []string) error { // Use respawn-pane to replace shell with Claude directly // Pass "gt prime" as initial prompt so Claude loads context immediately // Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes - bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name) - claudeCmd := fmt.Sprintf(`export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions "gt prime"`, r.Name, name, bdActor, bdActor) + claudeCmd := config.BuildCrewStartupCommand(r.Name, name, r.Path, "gt prime") if err := t.RespawnPane(paneID, claudeCmd); err != nil { return fmt.Errorf("restarting claude: %w", err) } diff --git a/internal/cmd/crew_lifecycle.go b/internal/cmd/crew_lifecycle.go index f07d14ad..fbeb3446 100644 --- a/internal/cmd/crew_lifecycle.go +++ b/internal/cmd/crew_lifecycle.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" + "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/crew" "github.com/steveyegge/gastown/internal/mail" @@ -335,8 +336,7 @@ func runCrewRestart(cmd *cobra.Command, args []string) error { // Start claude with skip permissions (crew workers are trusted) // Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes - bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name) - claudeCmd := fmt.Sprintf("export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions", r.Name, name, bdActor, bdActor) + claudeCmd := config.BuildCrewStartupCommand(r.Name, name, r.Path, "") if err := t.SendKeys(sessionID, claudeCmd); err != nil { fmt.Printf("Error starting claude for %s: %v\n", arg, err) lastErr = err @@ -511,8 +511,7 @@ func restartCrewSession(rigName, crewName, clonePath string) error { } // Start claude with skip permissions - bdActor := fmt.Sprintf("%s/crew/%s", rigName, crewName) - claudeCmd := fmt.Sprintf("export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions", rigName, crewName, bdActor, bdActor) + claudeCmd := config.BuildCrewStartupCommand(rigName, crewName, "", "") if err := t.SendKeys(sessionID, claudeCmd); err != nil { return fmt.Errorf("starting claude: %w", err) } diff --git a/internal/cmd/deacon.go b/internal/cmd/deacon.go index 8da4386e..ab8428e1 100644 --- a/internal/cmd/deacon.go +++ b/internal/cmd/deacon.go @@ -11,6 +11,7 @@ import ( "time" "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/deacon" "github.com/steveyegge/gastown/internal/polecat" @@ -291,7 +292,7 @@ func startDeaconSession(t *tmux.Tmux) error { // Restarts are handled by daemon via ensureDeaconRunning on each heartbeat // The startup hook handles context loading automatically // Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes - if err := t.SendKeys(DeaconSessionName, "export GT_ROLE=deacon BD_ACTOR=deacon GIT_AUTHOR_NAME=deacon && claude --dangerously-skip-permissions"); err != nil { + if err := t.SendKeys(DeaconSessionName, config.BuildAgentStartupCommand("deacon", "deacon", "", "")); err != nil { return fmt.Errorf("sending command: %w", err) } diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index 239c1c99..68dbf39a 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/events" "github.com/steveyegge/gastown/internal/session" "github.com/steveyegge/gastown/internal/style" @@ -327,10 +328,11 @@ func buildRestartCommand(sessionName string) (string, error) { // IMPORTANT: Passing "gt prime" as argument injects it as the first prompt, // which triggers the agent to execute immediately. Without this, agents // wait for user input despite all GUPP prompting in hooks. + runtimeCmd := config.GetRuntimeCommandWithPrompt("", "gt prime") if gtRole != "" { - return fmt.Sprintf("cd %s && export GT_ROLE=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && exec claude --dangerously-skip-permissions \"gt prime\"", workDir, gtRole, gtRole, gtRole), nil + return fmt.Sprintf("cd %s && export GT_ROLE=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && exec %s", workDir, gtRole, gtRole, gtRole, runtimeCmd), nil } - return fmt.Sprintf("cd %s && exec claude --dangerously-skip-permissions \"gt prime\"", workDir), nil + return fmt.Sprintf("cd %s && exec %s", workDir, runtimeCmd), nil } // sessionWorkDir returns the correct working directory for a session. diff --git a/internal/cmd/start.go b/internal/cmd/start.go index 880f5f5e..8cf42374 100644 --- a/internal/cmd/start.go +++ b/internal/cmd/start.go @@ -341,7 +341,8 @@ func ensureRefinerySession(rigName string, r *rig.Rig) (bool, error) { // Launch Claude in a respawn loop // Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes - loopCmd := `export GT_ROLE=refinery BD_ACTOR=` + bdActor + ` GIT_AUTHOR_NAME=` + bdActor + ` && while true; do echo "🛢️ Starting Refinery for ` + rigName + `..."; claude --dangerously-skip-permissions; echo ""; echo "Refinery exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done` + runtimeCmd := config.GetRuntimeCommand(r.Path) + loopCmd := `export GT_ROLE=refinery BD_ACTOR=` + bdActor + ` GIT_AUTHOR_NAME=` + bdActor + ` && while true; do echo "🛢️ Starting Refinery for ` + rigName + `..."; ` + runtimeCmd + `; echo ""; echo "Refinery exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done` if err := t.SendKeysDelayed(sessionName, loopCmd, 200); err != nil { return false, fmt.Errorf("sending command: %w", err) } @@ -744,7 +745,7 @@ func runStartCrew(cmd *cobra.Command, args []string) error { if !t.IsClaudeRunning(sessionID) { // Claude has exited, restart it fmt.Printf("Session exists, restarting Claude...\n") - if err := t.SendKeys(sessionID, "claude --dangerously-skip-permissions"); err != nil { + if err := t.SendKeys(sessionID, config.GetRuntimeCommand(r.Path)); err != nil { return fmt.Errorf("restarting claude: %w", err) } // Wait for Claude to start, then prime @@ -785,7 +786,7 @@ func runStartCrew(cmd *cobra.Command, args []string) error { } // Start claude with skip permissions - if err := t.SendKeys(sessionID, "claude --dangerously-skip-permissions"); err != nil { + if err := t.SendKeys(sessionID, config.GetRuntimeCommand(r.Path)); err != nil { return fmt.Errorf("starting claude: %w", err) } @@ -926,7 +927,7 @@ func startCrewMember(rigName, crewName, townRoot string) error { } // Start claude - if err := t.SendKeys(sessionID, "claude --dangerously-skip-permissions"); err != nil { + if err := t.SendKeys(sessionID, config.GetRuntimeCommand(r.Path)); err != nil { return fmt.Errorf("starting claude: %w", err) } diff --git a/internal/cmd/up.go b/internal/cmd/up.go index 5c06bde0..2b8266b1 100644 --- a/internal/cmd/up.go +++ b/internal/cmd/up.go @@ -262,11 +262,12 @@ func ensureSession(t *tmux.Tmux, sessionName, workDir, role string) error { // Launch Claude // Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes var claudeCmd string + runtimeCmd := config.GetRuntimeCommand("") if role == "deacon" { // Deacon uses respawn loop - claudeCmd = `export GT_ROLE=deacon BD_ACTOR=deacon GIT_AUTHOR_NAME=deacon && while true; do echo "⛪ Starting Deacon session..."; claude --dangerously-skip-permissions; echo ""; echo "Deacon exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done` + claudeCmd = `export GT_ROLE=deacon BD_ACTOR=deacon GIT_AUTHOR_NAME=deacon && while true; do echo "⛪ Starting Deacon session..."; ` + runtimeCmd + `; echo ""; echo "Deacon exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done` } else { - claudeCmd = fmt.Sprintf(`export GT_ROLE=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions`, role, role, role) + claudeCmd = config.BuildAgentStartupCommand(role, role, "", "") } if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil { diff --git a/internal/cmd/witness.go b/internal/cmd/witness.go index 34f5df66..2d681215 100644 --- a/internal/cmd/witness.go +++ b/internal/cmd/witness.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" "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/session" @@ -333,7 +334,7 @@ func ensureWitnessSession(rigName string, r *rig.Rig) (bool, error) { // Restarts are handled by daemon via LIFECYCLE mail or deacon health-scan // NOTE: No gt prime injection needed - SessionStart hook handles it automatically // Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes - if err := t.SendKeys(sessionName, fmt.Sprintf("export GT_ROLE=witness BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions", bdActor, bdActor)); err != nil { + if err := t.SendKeys(sessionName, config.BuildAgentStartupCommand("witness", bdActor, "", "")); err != nil { return false, fmt.Errorf("sending command: %w", err) } diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index b110a769..1dfc44e3 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -15,6 +15,7 @@ import ( "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/boot" + "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/feed" "github.com/steveyegge/gastown/internal/polecat" @@ -284,7 +285,7 @@ func (d *Daemon) ensureDeaconRunning() { // Launch Claude directly (no shell respawn loop) // The daemon will detect if Claude exits and restart it on next heartbeat // Export GT_ROLE and BD_ACTOR so Claude inherits them (tmux SetEnvironment doesn't export to processes) - if err := d.tmux.SendKeys(DeaconSessionName, "export GT_ROLE=deacon BD_ACTOR=deacon GIT_AUTHOR_NAME=deacon && claude --dangerously-skip-permissions"); err != nil { + if err := d.tmux.SendKeys(DeaconSessionName, config.BuildAgentStartupCommand("deacon", "deacon", "", "")); err != nil { d.logger.Printf("Error launching Claude in Deacon session: %v", err) return } @@ -333,8 +334,13 @@ func (d *Daemon) ensureWitnessRunning(rigName string) { // Launch Claude bdActor := fmt.Sprintf("%s/witness", rigName) - envExport := fmt.Sprintf("export GT_ROLE=witness GT_RIG=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions", rigName, bdActor, bdActor) - if err := d.tmux.SendKeys(sessionName, envExport); err != nil { + envVars := map[string]string{ + "GT_ROLE": "witness", + "GT_RIG": rigName, + "BD_ACTOR": bdActor, + "GIT_AUTHOR_NAME": bdActor, + } + if err := d.tmux.SendKeys(sessionName, config.BuildStartupCommand(envVars, "", "")); err != nil { d.logger.Printf("Error launching Claude in witness session for %s: %v", rigName, err) return } @@ -631,8 +637,7 @@ func (d *Daemon) restartPolecatSession(rigName, polecatName, sessionName string) _ = d.tmux.SetPaneDiedHook(sessionName, agentID) // Launch Claude with environment exported inline - startCmd := fmt.Sprintf("export GT_ROLE=polecat GT_RIG=%s GT_POLECAT=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions", - rigName, polecatName, bdActor, bdActor) + startCmd := config.BuildPolecatStartupCommand(rigName, polecatName, "", "") if err := d.tmux.SendKeys(sessionName, startCmd); err != nil { return fmt.Errorf("sending startup command: %w", err) } diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 24d416f3..1897a1c4 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -416,21 +416,19 @@ func (d *Daemon) getNeedsPreSync(config *beads.RoleConfig, parsed *ParsedIdentit // getStartCommand determines the startup command for an agent. // Uses role bead config if available, falls back to hardcoded defaults. -func (d *Daemon) getStartCommand(config *beads.RoleConfig, parsed *ParsedIdentity) string { +func (d *Daemon) getStartCommand(roleConfig *beads.RoleConfig, parsed *ParsedIdentity) string { // If role bead has explicit config, use it - if config != nil && config.StartCommand != "" { + if roleConfig != nil && roleConfig.StartCommand != "" { // Expand any patterns in the command - return beads.ExpandRolePattern(config.StartCommand, d.config.TownRoot, parsed.RigName, parsed.AgentName, parsed.RoleType) + return beads.ExpandRolePattern(roleConfig.StartCommand, d.config.TownRoot, parsed.RigName, parsed.AgentName, parsed.RoleType) } - // Default command for all agents - defaultCmd := "exec claude --dangerously-skip-permissions" + // Default command for all agents - use runtime config + defaultCmd := "exec " + config.GetRuntimeCommand("") // Polecats need environment variables set in the command if parsed.RoleType == "polecat" { - bdActor := fmt.Sprintf("%s/polecats/%s", parsed.RigName, parsed.AgentName) - return fmt.Sprintf("export GT_ROLE=polecat GT_RIG=%s GT_POLECAT=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && %s", - parsed.RigName, parsed.AgentName, bdActor, bdActor, defaultCmd) + return config.BuildPolecatStartupCommand(parsed.RigName, parsed.AgentName, "", "") } return defaultCmd diff --git a/internal/refinery/manager.go b/internal/refinery/manager.go index fae1ae50..1f10da84 100644 --- a/internal/refinery/manager.go +++ b/internal/refinery/manager.go @@ -192,7 +192,7 @@ func (m *Manager) Start(foreground bool) error { // Start Claude agent with full permissions (like polecats) // NOTE: No gt prime injection needed - SessionStart hook handles it automatically // Restarts are handled by daemon via LIFECYCLE mail, not shell loops - command := "claude --dangerously-skip-permissions" + command := config.GetRuntimeCommand("") if err := t.SendKeys(sessionID, command); err != nil { // Clean up the session on failure (best-effort cleanup) _ = t.KillSession(sessionID) diff --git a/internal/session/manager.go b/internal/session/manager.go index 53d3725c..76141e4d 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -11,6 +11,7 @@ import ( "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/tmux" @@ -178,9 +179,7 @@ func (m *Manager) Start(polecat string, opts StartOptions) error { if command == "" { // Polecats run with full permissions - Gas Town is for grownups // Export env vars inline so Claude's role detection works - bdActor := fmt.Sprintf("%s/polecats/%s", m.rig.Name, polecat) - command = fmt.Sprintf("export GT_ROLE=polecat GT_RIG=%s GT_POLECAT=%s BD_ACTOR=%s GIT_AUTHOR_NAME=%s && claude --dangerously-skip-permissions", - m.rig.Name, polecat, bdActor, bdActor) + command = config.BuildPolecatStartupCommand(m.rig.Name, polecat, m.rig.Path, "") } if err := m.tmux.SendKeys(sessionID, command); err != nil { return fmt.Errorf("sending command: %w", err)