Support multiple names in gt crew add

Examples:
  gt crew add murgen croaker goblin    # Create all three
  gt crew add dave                     # Still works for single

- Continues on failure (warns but doesn't abort)
- Shows summary at end
- Existing workspaces skipped with warning
This commit is contained in:
Steve Yegge
2025-12-30 19:43:09 -08:00
parent e2ce6148b7
commit db07394dbe
2 changed files with 96 additions and 58 deletions

View File

@@ -47,19 +47,20 @@ Commands:
var crewAddCmd = &cobra.Command{ var crewAddCmd = &cobra.Command{
Use: "add <name>", Use: "add <name>",
Short: "Create a new crew workspace", Short: "Create a new crew workspace",
Long: `Create a new crew workspace with a clone of the rig repository. Long: `Create new crew workspace(s) with a clone of the rig repository.
The workspace is created at <rig>/crew/<name>/ with: Each workspace is created at <rig>/crew/<name>/ with:
- A full git clone of the project repository - A full git clone of the project repository
- Mail directory for message delivery - Mail directory for message delivery
- CLAUDE.md with crew worker prompting - CLAUDE.md with crew worker prompting
- Optional feature branch (crew/<name>) - Optional feature branch (crew/<name>)
Examples: Examples:
gt crew add dave # Create in current rig gt crew add dave # Create single workspace
gt crew add murgen croaker goblin # Create multiple at once
gt crew add emma --rig greenplace # Create in specific rig gt crew add emma --rig greenplace # Create in specific rig
gt crew add fred --branch # Create with feature branch`, gt crew add fred --branch # Create with feature branch`,
Args: cobra.ExactArgs(1), Args: cobra.MinimumNArgs(1),
RunE: runCrewAdd, RunE: runCrewAdd,
} }

View File

@@ -15,19 +15,7 @@ import (
) )
func runCrewAdd(cmd *cobra.Command, args []string) error { func runCrewAdd(cmd *cobra.Command, args []string) error {
name := args[0] // Find workspace first (needed for all names)
// Parse rig/name format (e.g., "beads/emma" -> rig=beads, name=emma)
// This prevents creating nested directories like crew/beads/emma
rigName := crewRig
if parsedRig, crewName, ok := parseRigSlashName(name); ok {
if rigName == "" {
rigName = parsedRig
}
name = crewName
}
// Find workspace
townRoot, err := workspace.FindFromCwdOrError() townRoot, err := workspace.FindFromCwdOrError()
if err != nil { if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err) return fmt.Errorf("not in a Gas Town workspace: %w", err)
@@ -40,10 +28,17 @@ func runCrewAdd(cmd *cobra.Command, args []string) error {
rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)} rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)}
} }
// Determine rig (if not already set from slash format or --rig flag) // Determine base rig from --rig flag or first name's rig/name format
if rigName == "" { baseRig := crewRig
if baseRig == "" {
// Check if first arg has rig/name format
if parsedRig, _, ok := parseRigSlashName(args[0]); ok {
baseRig = parsedRig
}
}
if baseRig == "" {
// Try to infer from cwd // Try to infer from cwd
rigName, err = inferRigFromCwd(townRoot) baseRig, err = inferRigFromCwd(townRoot)
if err != nil { if err != nil {
return fmt.Errorf("could not determine rig (use --rig flag): %w", err) return fmt.Errorf("could not determine rig (use --rig flag): %w", err)
} }
@@ -52,56 +47,98 @@ func runCrewAdd(cmd *cobra.Command, args []string) error {
// Get rig // Get rig
g := git.NewGit(townRoot) g := git.NewGit(townRoot)
rigMgr := rig.NewManager(townRoot, rigsConfig, g) rigMgr := rig.NewManager(townRoot, rigsConfig, g)
r, err := rigMgr.GetRig(rigName) r, err := rigMgr.GetRig(baseRig)
if err != nil { if err != nil {
return fmt.Errorf("rig '%s' not found", rigName) return fmt.Errorf("rig '%s' not found", baseRig)
} }
// Create crew manager // Create crew manager
crewGit := git.NewGit(r.Path) crewGit := git.NewGit(r.Path)
crewMgr := crew.NewManager(r, crewGit) crewMgr := crew.NewManager(r, crewGit)
// Create crew workspace // Beads for agent bead creation
fmt.Printf("Creating crew workspace %s in %s...\n", name, rigName)
worker, err := crewMgr.Add(name, crewBranch)
if err != nil {
if err == crew.ErrCrewExists {
return fmt.Errorf("crew workspace '%s' already exists", name)
}
return fmt.Errorf("creating crew workspace: %w", err)
}
fmt.Printf("%s Created crew workspace: %s/%s\n",
style.Bold.Render("✓"), rigName, name)
fmt.Printf(" Path: %s\n", worker.ClonePath)
fmt.Printf(" Branch: %s\n", worker.Branch)
fmt.Printf(" Mail: %s/mail/\n", worker.ClonePath)
// Create agent bead for the crew worker
rigBeadsPath := filepath.Join(r.Path, "mayor", "rig") rigBeadsPath := filepath.Join(r.Path, "mayor", "rig")
bd := beads.New(rigBeadsPath) bd := beads.New(rigBeadsPath)
// Agent beads always use "gt-" prefix (required by beads validation)
// Only issue beads use rig-specific prefixes // Track results
crewID := beads.CrewBeadID(rigName, name) var created []string
if _, err := bd.Show(crewID); err != nil { var failed []string
// Agent bead doesn't exist, create it var lastWorker *crew.CrewWorker
fields := &beads.AgentFields{
RoleType: "crew", // Process each name
Rig: rigName, for _, arg := range args {
AgentState: "idle", name := arg
RoleBead: "gt-crew-role", rigName := baseRig
// Parse rig/name format (e.g., "beads/emma" -> rig=beads, name=emma)
if parsedRig, crewName, ok := parseRigSlashName(arg); ok {
// For rig/name format, use that rig (but warn if different from base)
if parsedRig != baseRig {
style.PrintWarning("%s: different rig '%s' ignored (use --rig to change)", arg, parsedRig)
}
name = crewName
} }
desc := fmt.Sprintf("Crew worker %s in %s - human-managed persistent workspace.", name, rigName)
if _, err := bd.CreateAgentBead(crewID, desc, fields); err != nil { // Create crew workspace
// Non-fatal: warn but don't fail the add fmt.Printf("Creating crew workspace %s in %s...\n", name, rigName)
style.PrintWarning("could not create agent bead: %v", err)
} else { worker, err := crewMgr.Add(name, crewBranch)
fmt.Printf(" Agent bead: %s\n", crewID) if err != nil {
if err == crew.ErrCrewExists {
style.PrintWarning("crew workspace '%s' already exists, skipping", name)
failed = append(failed, name+" (exists)")
continue
}
style.PrintWarning("creating crew workspace '%s': %v", name, err)
failed = append(failed, name)
continue
} }
fmt.Printf("%s Created crew workspace: %s/%s\n",
style.Bold.Render("✓"), rigName, name)
fmt.Printf(" Path: %s\n", worker.ClonePath)
fmt.Printf(" Branch: %s\n", worker.Branch)
// Create agent bead for the crew worker
crewID := beads.CrewBeadID(rigName, name)
if _, err := bd.Show(crewID); err != nil {
// Agent bead doesn't exist, create it
fields := &beads.AgentFields{
RoleType: "crew",
Rig: rigName,
AgentState: "idle",
RoleBead: "gt-crew-role",
}
desc := fmt.Sprintf("Crew worker %s in %s - human-managed persistent workspace.", name, rigName)
if _, err := bd.CreateAgentBead(crewID, desc, fields); err != nil {
style.PrintWarning("could not create agent bead for %s: %v", name, err)
} else {
fmt.Printf(" Agent bead: %s\n", crewID)
}
}
created = append(created, name)
lastWorker = worker
fmt.Println()
} }
fmt.Printf("\n%s\n", style.Dim.Render("Start working with: cd "+worker.ClonePath)) // Summary
if len(created) > 0 {
fmt.Printf("%s Created %d crew workspace(s): %v\n",
style.Bold.Render("✓"), len(created), created)
if lastWorker != nil && len(created) == 1 {
fmt.Printf("\n%s\n", style.Dim.Render("Start working with: cd "+lastWorker.ClonePath))
}
}
if len(failed) > 0 {
fmt.Printf("%s Failed to create %d workspace(s): %v\n",
style.Warning.Render("!"), len(failed), failed)
}
// Return error if all failed
if len(created) == 0 && len(failed) > 0 {
return fmt.Errorf("failed to create any crew workspaces")
}
return nil return nil
} }