diff --git a/internal/cmd/crew_at.go b/internal/cmd/crew_at.go index e6c57f51..18b75c3b 100644 --- a/internal/cmd/crew_at.go +++ b/internal/cmd/crew_at.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "time" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/config" @@ -106,7 +105,7 @@ func runCrewAt(cmd *cobra.Command, args []string) error { _ = t.ConfigureGasTownSession(sessionID, theme, r.Name, name, "crew") // Wait for shell to be ready after session creation - if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil { + if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil { return fmt.Errorf("waiting for shell: %w", err) } diff --git a/internal/cmd/crew_lifecycle.go b/internal/cmd/crew_lifecycle.go index 128956b0..27fd2c3f 100644 --- a/internal/cmd/crew_lifecycle.go +++ b/internal/cmd/crew_lifecycle.go @@ -141,7 +141,7 @@ func runCrewRefresh(cmd *cobra.Command, args []string) error { _ = t.SetEnvironment(sessionID, "GT_CREW", name) // Wait for shell to be ready - if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil { + if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil { return fmt.Errorf("waiting for shell: %w", err) } @@ -236,7 +236,7 @@ func runCrewRestart(cmd *cobra.Command, args []string) error { _ = t.ConfigureGasTownSession(sessionID, theme, r.Name, name, "crew") // Wait for shell to be ready - if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil { + if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil { return fmt.Errorf("waiting for shell: %w", err) } @@ -250,11 +250,11 @@ func runCrewRestart(cmd *cobra.Command, args []string) error { // Wait for Claude to start, then prime it shells := constants.SupportedShells - if err := t.WaitForCommand(sessionID, shells, 15*time.Second); err != nil { + if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil { style.PrintWarning("Timeout waiting for Claude to start: %v", err) } // Give Claude time to initialize after process starts - time.Sleep(500 * time.Millisecond) + time.Sleep(constants.ShutdownNotifyDelay) if err := t.SendKeys(sessionID, "gt prime"); err != nil { // Non-fatal: Claude started but priming failed style.PrintWarning("Could not send prime command: %v", err) @@ -358,7 +358,7 @@ func runCrewRestartAll() error { crewRig = savedRig // Small delay between restarts to avoid overwhelming the system - time.Sleep(500 * time.Millisecond) + time.Sleep(constants.ShutdownNotifyDelay) } fmt.Println() @@ -402,7 +402,7 @@ func restartCrewSession(rigName, crewName, clonePath string) error { _ = t.ConfigureGasTownSession(sessionID, theme, rigName, crewName, "crew") // Wait for shell to be ready - if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil { + if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil { return fmt.Errorf("waiting for shell: %w", err) } @@ -415,10 +415,10 @@ func restartCrewSession(rigName, crewName, clonePath string) error { // Wait for Claude to start, then prime it shells := constants.SupportedShells - if err := t.WaitForCommand(sessionID, shells, 15*time.Second); err != nil { + if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil { // Non-fatal warning } - time.Sleep(500 * time.Millisecond) + time.Sleep(constants.ShutdownNotifyDelay) if err := t.SendKeys(sessionID, "gt prime"); err != nil { // Non-fatal } diff --git a/internal/cmd/start.go b/internal/cmd/start.go index 6fc543c6..2c312971 100644 --- a/internal/cmd/start.go +++ b/internal/cmd/start.go @@ -459,7 +459,7 @@ func runGracefulShutdown(t *tmux.Tmux, gtSessions []string, townRoot string) err shutdownMsg := "[SHUTDOWN] Gas Town is shutting down. Please save your state and update your handoff bead, then type /exit or wait to be terminated." for _, sess := range gtSessions { // Small delay then send the message - time.Sleep(500 * time.Millisecond) + time.Sleep(constants.ShutdownNotifyDelay) _ = t.SendKeys(sess, shutdownMsg) // best-effort notification } @@ -748,10 +748,10 @@ func runStartCrew(cmd *cobra.Command, args []string) error { } // Wait for Claude to start, then prime shells := constants.SupportedShells - if err := t.WaitForCommand(sessionID, shells, 15*time.Second); err != nil { + if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil { style.PrintWarning("Timeout waiting for Claude to start: %v", err) } - time.Sleep(500 * time.Millisecond) + time.Sleep(constants.ShutdownNotifyDelay) if err := t.SendKeys(sessionID, "gt prime"); err != nil { style.PrintWarning("Could not send prime command: %v", err) } @@ -779,7 +779,7 @@ func runStartCrew(cmd *cobra.Command, args []string) error { _ = t.ConfigureGasTownSession(sessionID, theme, rigName, name, "crew") // Wait for shell to be ready after session creation - if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil { + if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil { return fmt.Errorf("waiting for shell: %w", err) } @@ -790,12 +790,12 @@ func runStartCrew(cmd *cobra.Command, args []string) error { // Wait for Claude to start shells := constants.SupportedShells - if err := t.WaitForCommand(sessionID, shells, 15*time.Second); err != nil { + if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil { style.PrintWarning("Timeout waiting for Claude to start: %v", err) } // Give Claude time to initialize after process starts - time.Sleep(500 * time.Millisecond) + time.Sleep(constants.ShutdownNotifyDelay) // Send gt prime to initialize context if err := t.SendKeys(sessionID, "gt prime"); err != nil { @@ -913,7 +913,7 @@ func startCrewMember(rigName, crewName, townRoot string) error { _ = t.SetCrewCycleBindings(sessionID) // Wait for shell to be ready - if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil { + if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil { return fmt.Errorf("waiting for shell: %w", err) } @@ -924,12 +924,12 @@ func startCrewMember(rigName, crewName, townRoot string) error { // Wait for Claude to start shells := constants.SupportedShells - if err := t.WaitForCommand(sessionID, shells, 15*time.Second); err != nil { + if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil { // Non-fatal: Claude might still be starting } // Give Claude time to initialize - time.Sleep(500 * time.Millisecond) + time.Sleep(constants.ShutdownNotifyDelay) // Send gt prime to initialize context (non-fatal: session works without priming) _ = t.SendKeys(sessionID, "gt prime") diff --git a/internal/constants/constants.go b/internal/constants/constants.go index a7284914..7685514a 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -2,6 +2,29 @@ // Centralizing these magic strings improves maintainability and consistency. package constants +import "time" + +// Timing constants for session management and tmux operations. +const ( + // ShutdownNotifyDelay is the pause after sending shutdown notification. + ShutdownNotifyDelay = 500 * time.Millisecond + + // ClaudeStartTimeout is how long to wait for Claude to start in a session. + ClaudeStartTimeout = 15 * time.Second + + // ShellReadyTimeout is how long to wait for shell prompt after command. + ShellReadyTimeout = 5 * time.Second + + // DefaultDebounceMs is the default debounce for SendKeys operations. + DefaultDebounceMs = 100 + + // DefaultDisplayMs is the default duration for tmux display-message. + DefaultDisplayMs = 5000 + + // PollInterval is the default polling interval for wait loops. + PollInterval = 100 * time.Millisecond +) + // Directory names within a Gas Town workspace. const ( // DirMayor is the directory containing mayor configuration and state. diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index aab1523f..ae89b9ac 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -12,6 +12,7 @@ import ( "syscall" "time" + "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/keepalive" "github.com/steveyegge/gastown/internal/polecat" "github.com/steveyegge/gastown/internal/tmux" @@ -547,7 +548,7 @@ func StopDaemon(townRoot string) error { } // Wait a bit for graceful shutdown - time.Sleep(500 * time.Millisecond) + time.Sleep(constants.ShutdownNotifyDelay) // Check if still running if err := process.Signal(syscall.Signal(0)); err == nil { diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 54cdbad1..89f2b440 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -10,6 +10,7 @@ import ( "time" "github.com/steveyegge/gastown/internal/beads" + "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/tmux" ) @@ -195,7 +196,7 @@ func (d *Daemon) executeLifecycleAction(request *LifecycleRequest) error { d.logger.Printf("Killed session %s for restart", sessionName) // Wait a moment - time.Sleep(500 * time.Millisecond) + time.Sleep(constants.ShutdownNotifyDelay) } // Restart the session diff --git a/internal/tmux/tmux.go b/internal/tmux/tmux.go index c794c8bb..c04b0dd9 100644 --- a/internal/tmux/tmux.go +++ b/internal/tmux/tmux.go @@ -155,7 +155,7 @@ func (t *Tmux) ListSessionIDs() (map[string]string, error) { // Always sends Enter as a separate command for reliability. // Uses a debounce delay between paste and Enter to ensure paste completes. func (t *Tmux) SendKeys(session, keys string) error { - return t.SendKeysDebounced(session, keys, 100) // 100ms default debounce + return t.SendKeysDebounced(session, keys, constants.DefaultDebounceMs) // 100ms default debounce } // SendKeysDebounced sends keystrokes with a configurable delay before Enter. @@ -375,7 +375,7 @@ func (t *Tmux) DisplayMessage(session, message string, durationMs int) error { // DisplayMessageDefault shows a message with default duration (5 seconds). func (t *Tmux) DisplayMessageDefault(session, message string) error { - return t.DisplayMessage(session, message, 5000) + return t.DisplayMessage(session, message, constants.DefaultDisplayMs) } // SendNotificationBanner sends a visible notification banner to a tmux session. @@ -413,7 +413,7 @@ func (t *Tmux) WaitForCommand(session string, excludeCommands []string, timeout for time.Now().Before(deadline) { cmd, err := t.GetPaneCommand(session) if err != nil { - time.Sleep(100 * time.Millisecond) + time.Sleep(constants.PollInterval) continue } // Check if current command is NOT in the exclude list @@ -427,7 +427,7 @@ func (t *Tmux) WaitForCommand(session string, excludeCommands []string, timeout if !excluded { return nil } - time.Sleep(100 * time.Millisecond) + time.Sleep(constants.PollInterval) } return fmt.Errorf("timeout waiting for command (still running excluded command)") } @@ -440,7 +440,7 @@ func (t *Tmux) WaitForShellReady(session string, timeout time.Duration) error { for time.Now().Before(deadline) { cmd, err := t.GetPaneCommand(session) if err != nil { - time.Sleep(100 * time.Millisecond) + time.Sleep(constants.PollInterval) continue } for _, shell := range shells { @@ -448,7 +448,7 @@ func (t *Tmux) WaitForShellReady(session string, timeout time.Duration) error { return nil } } - time.Sleep(100 * time.Millisecond) + time.Sleep(constants.PollInterval) } return fmt.Errorf("timeout waiting for shell") }