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 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/max
2026-01-09 21:24:23 -08:00
committed by Steve Yegge
parent be35b3eaab
commit cb2b130ca2
+63 -15
View File
@@ -1,15 +1,18 @@
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/constants"
"github.com/steveyegge/gastown/internal/crew" "github.com/steveyegge/gastown/internal/crew"
"github.com/steveyegge/gastown/internal/mail" "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 var lastErr error
startedCount := 0 startedCount := 0
for _, name := range crewNames { skippedCount := 0
// Set the start.go flags before calling runStartCrew for res := range results {
startCrewRig = rigName if res.err != nil {
startCrewAccount = crewAccount fmt.Printf(" %s %s/%s: %v\n", style.ErrorPrefix, rigName, res.name, res.err)
startCrewAgentOverride = crewAgentOverride lastErr = res.err
} else if res.skipped {
// Use rig/name format for runStartCrew fmt.Printf(" %s %s/%s: already running\n", style.Dim.Render("○"), rigName, res.name)
fullName := rigName + "/" + name skippedCount++
if err := runStartCrew(cmd, []string{fullName}); err != nil {
fmt.Printf("Error starting %s/%s: %v\n", rigName, name, err)
lastErr = err
} else { } else {
fmt.Printf(" %s %s/%s: started\n", style.SuccessPrefix, rigName, res.name)
startedCount++ startedCount++
} }
} }
if startedCount > 0 { // Summary
fmt.Printf("\n%s Started %d crew member(s) in %s\n", fmt.Println()
style.Bold.Render("✓"), startedCount, r.Name) 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 return lastErr