From cb2b130ca20da9aeb1f19cc46e55631700f95b6c Mon Sep 17 00:00:00 2001 From: gastown/crew/max Date: Fri, 9 Jan 2026 21:24:23 -0800 Subject: [PATCH] fix(crew): parallelize crew start to prevent hanging Start crew members concurrently instead of sequentially. Previously, `gt crew start --all` could hang for minutes because each crew member was started one at a time, with each waiting up to 60 seconds for Claude to initialize. With parallel startup, all crew members start simultaneously and the total wait time is bounded by the slowest individual startup. Co-Authored-By: Claude Opus 4.5 --- internal/cmd/crew_lifecycle.go | 78 +++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/internal/cmd/crew_lifecycle.go b/internal/cmd/crew_lifecycle.go index c55ded1f..d95db2a3 100644 --- a/internal/cmd/crew_lifecycle.go +++ b/internal/cmd/crew_lifecycle.go @@ -1,15 +1,18 @@ package cmd import ( + "errors" "fmt" "os" "os/exec" "path/filepath" "strings" + "sync" "time" "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" @@ -300,28 +303,73 @@ func runCrewStart(cmd *cobra.Command, args []string) error { } } - // Start each crew member + // Resolve account config once for all crew members + townRoot, _ := workspace.Find(r.Path) + if townRoot == "" { + townRoot = filepath.Dir(r.Path) + } + accountsPath := constants.MayorAccountsPath(townRoot) + claudeConfigDir, _, _ := config.ResolveAccountConfigDir(accountsPath, crewAccount) + + // Build start options (shared across all crew members) + opts := crew.StartOptions{ + Account: crewAccount, + ClaudeConfigDir: claudeConfigDir, + AgentOverride: crewAgentOverride, + } + + // Start each crew member in parallel + type result struct { + name string + err error + skipped bool // true if session was already running + } + results := make(chan result, len(crewNames)) + var wg sync.WaitGroup + + fmt.Printf("Starting %d crew member(s) in %s...\n", len(crewNames), rigName) + + for _, name := range crewNames { + wg.Add(1) + go func(crewName string) { + defer wg.Done() + err := crewMgr.Start(crewName, opts) + skipped := errors.Is(err, crew.ErrSessionRunning) + if skipped { + err = nil // Not an error, just already running + } + results <- result{name: crewName, err: err, skipped: skipped} + }(name) + } + + // Wait for all goroutines to complete + go func() { + wg.Wait() + close(results) + }() + + // Collect results var lastErr error startedCount := 0 - for _, name := range crewNames { - // Set the start.go flags before calling runStartCrew - startCrewRig = rigName - startCrewAccount = crewAccount - startCrewAgentOverride = crewAgentOverride - - // Use rig/name format for runStartCrew - fullName := rigName + "/" + name - if err := runStartCrew(cmd, []string{fullName}); err != nil { - fmt.Printf("Error starting %s/%s: %v\n", rigName, name, err) - lastErr = err + skippedCount := 0 + for res := range results { + if res.err != nil { + fmt.Printf(" %s %s/%s: %v\n", style.ErrorPrefix, rigName, res.name, res.err) + lastErr = res.err + } else if res.skipped { + fmt.Printf(" %s %s/%s: already running\n", style.Dim.Render("○"), rigName, res.name) + skippedCount++ } else { + fmt.Printf(" %s %s/%s: started\n", style.SuccessPrefix, rigName, res.name) startedCount++ } } - if startedCount > 0 { - fmt.Printf("\n%s Started %d crew member(s) in %s\n", - style.Bold.Render("✓"), startedCount, r.Name) + // Summary + fmt.Println() + if startedCount > 0 || skippedCount > 0 { + fmt.Printf("%s Started %d, skipped %d (already running) in %s\n", + style.Bold.Render("✓"), startedCount, skippedCount, r.Name) } return lastErr