feat: add --agent overrides to start/attach

This commit is contained in:
jv
2026-01-07 12:36:35 +13:00
committed by Steve Yegge
parent 3b9ca71fc4
commit 6afd85df4b
7 changed files with 168 additions and 45 deletions

View File

@@ -8,17 +8,18 @@ import (
// Crew command flags
var (
crewRig string
crewBranch bool
crewJSON bool
crewForce bool
crewPurge bool
crewNoTmux bool
crewDetached bool
crewMessage string
crewAccount string
crewAll bool
crewDryRun bool
crewRig string
crewBranch bool
crewJSON bool
crewForce bool
crewPurge bool
crewNoTmux bool
crewDetached bool
crewMessage string
crewAccount string
crewAgentOverride string
crewAll bool
crewDryRun bool
)
var crewCmd = &cobra.Command{
@@ -328,6 +329,7 @@ func init() {
crewAtCmd.Flags().BoolVar(&crewNoTmux, "no-tmux", false, "Just print directory path")
crewAtCmd.Flags().BoolVarP(&crewDetached, "detached", "d", false, "Start session without attaching")
crewAtCmd.Flags().StringVar(&crewAccount, "account", "", "Claude Code account handle to use (overrides default)")
crewAtCmd.Flags().StringVar(&crewAgentOverride, "agent", "", "Agent alias to run crew worker with (overrides rig/town default)")
crewRemoveCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use")
crewRemoveCmd.Flags().BoolVar(&crewForce, "force", false, "Force remove (skip safety checks)")
@@ -350,6 +352,7 @@ func init() {
crewStartCmd.Flags().BoolVar(&crewAll, "all", false, "Start all crew members in the rig")
crewStartCmd.Flags().StringVar(&crewAccount, "account", "", "Claude Code account handle to use")
crewStartCmd.Flags().StringVar(&crewAgentOverride, "agent", "", "Agent alias to run crew worker with (overrides rig/town default)")
crewStopCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use (filter when using --all)")
crewStopCmd.Flags().BoolVar(&crewAll, "all", false, "Stop all running crew sessions")

View File

@@ -150,8 +150,11 @@ 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
claudeCmd := config.BuildCrewStartupCommand(r.Name, name, r.Path, "gt prime")
if err := t.RespawnPane(paneID, claudeCmd); err != nil {
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(r.Name, name, r.Path, "gt prime", crewAgentOverride)
if err != nil {
return fmt.Errorf("building startup command: %w", err)
}
if err := t.RespawnPane(paneID, startupCmd); err != nil {
return fmt.Errorf("starting claude: %w", err)
}
@@ -174,8 +177,11 @@ 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
claudeCmd := config.BuildCrewStartupCommand(r.Name, name, r.Path, "gt prime")
if err := t.RespawnPane(paneID, claudeCmd); err != nil {
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(r.Name, name, r.Path, "gt prime", crewAgentOverride)
if err != nil {
return fmt.Errorf("building startup command: %w", err)
}
if err := t.RespawnPane(paneID, startupCmd); err != nil {
return fmt.Errorf("restarting claude: %w", err)
}
}
@@ -185,7 +191,10 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
if isInTmuxSession(sessionID) {
// We're in the session at a shell prompt - just start the agent directly
// Pass "gt prime" as initial prompt so it loads context immediately
agentCfg := config.ResolveAgentConfig(townRoot, r.Path)
agentCfg, _, err := config.ResolveAgentConfigWithOverride(townRoot, r.Path, crewAgentOverride)
if err != nil {
return fmt.Errorf("resolving agent: %w", err)
}
fmt.Printf("Starting %s in current session...\n", agentCfg.Command)
return execAgent(agentCfg, "gt prime")
}

View File

@@ -338,6 +338,7 @@ func runCrewStart(cmd *cobra.Command, args []string) error {
// Set the start.go flags before calling runStartCrew
startCrewRig = rigName
startCrewAccount = crewAccount
startCrewAgentOverride = crewAgentOverride
// Use rig/name format for runStartCrew
fullName := rigName + "/" + name

View File

@@ -89,6 +89,8 @@ Stops the current session (if running) and starts a fresh one.`,
RunE: runDeaconRestart,
}
var deaconAgentOverride string
var deaconHeartbeatCmd = &cobra.Command{
Use: "heartbeat [action]",
Short: "Update the Deacon heartbeat",
@@ -203,7 +205,6 @@ Examples:
RunE: runDeaconStaleHooks,
}
var (
triggerTimeout time.Duration
@@ -258,6 +259,10 @@ func init() {
deaconStaleHooksCmd.Flags().BoolVar(&staleHooksDryRun, "dry-run", false,
"Preview what would be unhooked without making changes")
deaconStartCmd.Flags().StringVar(&deaconAgentOverride, "agent", "", "Agent alias to run the Deacon with (overrides town default)")
deaconAttachCmd.Flags().StringVar(&deaconAgentOverride, "agent", "", "Agent alias to run the Deacon with (overrides town default)")
deaconRestartCmd.Flags().StringVar(&deaconAgentOverride, "agent", "", "Agent alias to run the Deacon with (overrides town default)")
rootCmd.AddCommand(deaconCmd)
}
@@ -275,7 +280,7 @@ func runDeaconStart(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Deacon session already running. Attach with: gt deacon attach")
}
if err := startDeaconSession(t, sessionName); err != nil {
if err := startDeaconSession(t, sessionName, deaconAgentOverride); err != nil {
return err
}
@@ -287,7 +292,7 @@ func runDeaconStart(cmd *cobra.Command, args []string) error {
}
// startDeaconSession creates and initializes the Deacon tmux session.
func startDeaconSession(t *tmux.Tmux, sessionName string) error {
func startDeaconSession(t *tmux.Tmux, sessionName, agentOverride string) error {
// Find workspace root
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
@@ -326,7 +331,11 @@ func startDeaconSession(t *tmux.Tmux, sessionName string) 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(sessionName, config.BuildAgentStartupCommand("deacon", "deacon", "", "")); err != nil {
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("deacon", "deacon", "", "", agentOverride)
if err != nil {
return fmt.Errorf("building startup command: %w", err)
}
if err := t.SendKeys(sessionName, startupCmd); err != nil {
return fmt.Errorf("sending command: %w", err)
}
@@ -394,7 +403,7 @@ func runDeaconAttach(cmd *cobra.Command, args []string) error {
if !running {
// Auto-start if not running
fmt.Println("Deacon session not running, starting...")
if err := startDeaconSession(t, sessionName); err != nil {
if err := startDeaconSession(t, sessionName, deaconAgentOverride); err != nil {
return err
}
}
@@ -942,4 +951,3 @@ func runDeaconStaleHooks(cmd *cobra.Command, args []string) error {
return nil
}

View File

@@ -31,6 +31,8 @@ The Mayor is the global coordinator for Gas Town, running as a persistent
tmux session. Use the subcommands to start, stop, attach, and check status.`,
}
var mayorAgentOverride string
var mayorStartCmd = &cobra.Command{
Use: "start",
Short: "Start the Mayor session",
@@ -84,6 +86,10 @@ func init() {
mayorCmd.AddCommand(mayorStatusCmd)
mayorCmd.AddCommand(mayorRestartCmd)
mayorStartCmd.Flags().StringVar(&mayorAgentOverride, "agent", "", "Agent alias to run the Mayor with (overrides town default)")
mayorAttachCmd.Flags().StringVar(&mayorAgentOverride, "agent", "", "Agent alias to run the Mayor with (overrides town default)")
mayorRestartCmd.Flags().StringVar(&mayorAgentOverride, "agent", "", "Agent alias to run the Mayor with (overrides town default)")
rootCmd.AddCommand(mayorCmd)
}
@@ -101,7 +107,7 @@ func runMayorStart(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Mayor session already running. Attach with: gt mayor attach")
}
if err := startMayorSession(t, sessionName); err != nil {
if err := startMayorSession(t, sessionName, mayorAgentOverride); err != nil {
return err
}
@@ -113,7 +119,7 @@ func runMayorStart(cmd *cobra.Command, args []string) error {
}
// startMayorSession creates and initializes the Mayor tmux session.
func startMayorSession(t *tmux.Tmux, sessionName string) error {
func startMayorSession(t *tmux.Tmux, sessionName, agentOverride string) error {
// Find workspace root
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
@@ -139,8 +145,11 @@ func startMayorSession(t *tmux.Tmux, sessionName string) error {
// Use SendKeysDelayed to allow shell initialization after NewSession
// Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
// Mayor uses default runtime config (empty rigPath) since it's not rig-specific
claudeCmd := config.BuildAgentStartupCommand("mayor", "mayor", "", "")
if err := t.SendKeysDelayed(sessionName, claudeCmd, 200); err != nil {
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("mayor", "mayor", "", "", agentOverride)
if err != nil {
return fmt.Errorf("building startup command: %w", err)
}
if err := t.SendKeysDelayed(sessionName, startupCmd, 200); err != nil {
return fmt.Errorf("sending command: %w", err)
}
@@ -208,7 +217,7 @@ func runMayorAttach(cmd *cobra.Command, args []string) error {
if !running {
// Auto-start if not running
fmt.Println("Mayor session not running, starting...")
if err := startMayorSession(t, sessionName); err != nil {
if err := startMayorSession(t, sessionName, mayorAgentOverride); err != nil {
return err
}
}

View File

@@ -24,16 +24,18 @@ import (
)
var (
startAll bool
startCrewRig string
startCrewAccount string
shutdownGraceful bool
shutdownWait int
shutdownAll bool
shutdownForce bool
shutdownYes bool
shutdownPolecatsOnly bool
shutdownNuclear bool
startAll bool
startAgentOverride string
startCrewRig string
startCrewAccount string
startCrewAgentOverride string
shutdownGraceful bool
shutdownWait int
shutdownAll bool
shutdownForce bool
shutdownYes bool
shutdownPolecatsOnly bool
shutdownNuclear bool
)
var startCmd = &cobra.Command{
@@ -104,9 +106,11 @@ Examples:
func init() {
startCmd.Flags().BoolVarP(&startAll, "all", "a", false,
"Also start Witnesses and Refineries for all rigs")
startCmd.Flags().StringVar(&startAgentOverride, "agent", "", "Agent alias to run Mayor/Deacon with (overrides town default)")
startCrewCmd.Flags().StringVar(&startCrewRig, "rig", "", "Rig to use")
startCrewCmd.Flags().StringVar(&startCrewAccount, "account", "", "Claude Code account handle to use")
startCrewCmd.Flags().StringVar(&startCrewAgentOverride, "agent", "", "Agent alias to run crew worker with (overrides rig/town default)")
startCmd.AddCommand(startCrewCmd)
shutdownCmd.Flags().BoolVarP(&shutdownGraceful, "graceful", "g", false,
@@ -155,7 +159,7 @@ func runStart(cmd *cobra.Command, args []string) error {
fmt.Printf("Starting Gas Town from %s\n\n", style.Dim.Render(townRoot))
// Start core agents (Mayor and Deacon)
if err := startCoreAgents(t); err != nil {
if err := startCoreAgents(t, startAgentOverride); err != nil {
return err
}
@@ -182,7 +186,7 @@ func runStart(cmd *cobra.Command, args []string) error {
}
// startCoreAgents starts Mayor and Deacon sessions.
func startCoreAgents(t *tmux.Tmux) error {
func startCoreAgents(t *tmux.Tmux, agentOverride string) error {
// Get session names
mayorSession := getMayorSessionName()
deaconSession := getDeaconSessionName()
@@ -193,7 +197,7 @@ func startCoreAgents(t *tmux.Tmux) error {
fmt.Printf(" %s Mayor already running\n", style.Dim.Render("○"))
} else {
fmt.Printf(" %s Starting Mayor...\n", style.Bold.Render("→"))
if err := startMayorSession(t, mayorSession); err != nil {
if err := startMayorSession(t, mayorSession, agentOverride); err != nil {
return fmt.Errorf("starting Mayor: %w", err)
}
fmt.Printf(" %s Mayor started\n", style.Bold.Render("✓"))
@@ -205,7 +209,7 @@ func startCoreAgents(t *tmux.Tmux) error {
fmt.Printf(" %s Deacon already running\n", style.Dim.Render("○"))
} else {
fmt.Printf(" %s Starting Deacon...\n", style.Bold.Render("→"))
if err := startDeaconSession(t, deaconSession); err != nil {
if err := startDeaconSession(t, deaconSession, agentOverride); err != nil {
return fmt.Errorf("starting Deacon: %w", err)
}
fmt.Printf(" %s Deacon started\n", style.Bold.Render("✓"))
@@ -799,8 +803,11 @@ func runStartCrew(cmd *cobra.Command, args []string) error {
if !t.IsClaudeRunning(sessionID) {
// Claude has exited, restart it with "gt prime" as initial prompt
fmt.Printf("Session exists, restarting Claude...\n")
claudeCmd := config.BuildCrewStartupCommand(rigName, name, r.Path, "gt prime")
if err := t.SendKeys(sessionID, claudeCmd); err != nil {
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(rigName, name, r.Path, "gt prime", startCrewAgentOverride)
if err != nil {
return fmt.Errorf("building startup command: %w", err)
}
if err := t.SendKeys(sessionID, startupCmd); err != nil {
return fmt.Errorf("restarting claude: %w", err)
}
} else {
@@ -833,8 +840,11 @@ func runStartCrew(cmd *cobra.Command, args []string) error {
// Start claude with skip permissions and proper env vars for seance
// Pass "gt prime" as initial prompt so context is loaded immediately
claudeCmd := config.BuildCrewStartupCommand(rigName, name, r.Path, "gt prime")
if err := t.SendKeys(sessionID, claudeCmd); err != nil {
startupCmd, err := config.BuildCrewStartupCommandWithAgentOverride(rigName, name, r.Path, "gt prime", startCrewAgentOverride)
if err != nil {
return fmt.Errorf("building startup command: %w", err)
}
if err := t.SendKeys(sessionID, startupCmd); err != nil {
return fmt.Errorf("starting claude: %w", err)
}