From defc97216f22755cd573147610d8cd21e0be54ac Mon Sep 17 00:00:00 2001 From: nux Date: Sat, 24 Jan 2026 16:48:51 -0800 Subject: [PATCH] feat(crew): add crew configuration to rigs.json for cross-machine sync Add CrewRegistryConfig to RigEntry allowing crew members to be defined in rigs.json and synced across machines. The new `gt crew sync` command creates missing crew members from the configuration. Configuration example: "rigs": { "gastown": { "crew": { "theme": "mad-max", "members": ["diesel", "chrome", "nitro"] } } } Closes: gt-tu4 --- internal/cmd/crew_sync.go | 204 ++++++++++++++++++++++++++++++++++++++ internal/config/types.go | 21 +++- 2 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 internal/cmd/crew_sync.go diff --git a/internal/cmd/crew_sync.go b/internal/cmd/crew_sync.go new file mode 100644 index 00000000..df4fa5c2 --- /dev/null +++ b/internal/cmd/crew_sync.go @@ -0,0 +1,204 @@ +package cmd + +import ( + "fmt" + "path/filepath" + + "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/beads" + "github.com/steveyegge/gastown/internal/config" + "github.com/steveyegge/gastown/internal/crew" + "github.com/steveyegge/gastown/internal/git" + "github.com/steveyegge/gastown/internal/rig" + "github.com/steveyegge/gastown/internal/style" + "github.com/steveyegge/gastown/internal/workspace" +) + +var crewSyncCmd = &cobra.Command{ + Use: "sync", + Short: "Create missing crew members from rigs.json config", + Long: `Sync crew members from rigs.json configuration. + +Creates any crew members defined in rigs.json that don't already exist locally. +This enables sharing crew configuration across machines. + +Configuration in mayor/rigs.json: + { + "rigs": { + "gastown": { + "crew": { + "theme": "mad-max", + "members": ["diesel", "chrome", "nitro"] + } + } + } + } + +Examples: + gt crew sync # Sync crew in current rig + gt crew sync --rig gastown # Sync crew in specific rig + gt crew sync --dry-run # Show what would be created`, + RunE: runCrewSync, +} + +func init() { + crewSyncCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to sync crew in") + crewSyncCmd.Flags().BoolVar(&crewDryRun, "dry-run", false, "Show what would be created without creating") + crewCmd.AddCommand(crewSyncCmd) +} + +func runCrewSync(cmd *cobra.Command, args []string) error { + // Find workspace + townRoot, err := workspace.FindFromCwdOrError() + if err != nil { + return fmt.Errorf("not in a Gas Town workspace: %w", err) + } + + // Load rigs config + rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json") + rigsConfig, err := config.LoadRigsConfig(rigsConfigPath) + if err != nil { + return fmt.Errorf("loading rigs config: %w", err) + } + + // Determine rig + rigName := crewRig + if rigName == "" { + rigName, err = inferRigFromCwd(townRoot) + if err != nil { + return fmt.Errorf("could not determine rig (use --rig flag): %w", err) + } + } + + // Get rig entry from rigs.json + rigEntry, ok := rigsConfig.Rigs[rigName] + if !ok { + return fmt.Errorf("rig '%s' not found in rigs.json", rigName) + } + + // Check if crew config exists + if rigEntry.Crew == nil || len(rigEntry.Crew.Members) == 0 { + fmt.Printf("No crew members configured for rig '%s' in rigs.json\n", rigName) + fmt.Printf("\nTo configure crew, add to mayor/rigs.json:\n") + fmt.Printf(" \"crew\": {\n") + fmt.Printf(" \"theme\": \"mad-max\",\n") + fmt.Printf(" \"members\": [\"diesel\", \"chrome\", \"nitro\"]\n") + fmt.Printf(" }\n") + return nil + } + + // Get rig + g := git.NewGit(townRoot) + rigMgr := rig.NewManager(townRoot, rigsConfig, g) + r, err := rigMgr.GetRig(rigName) + if err != nil { + return fmt.Errorf("rig '%s' not found", rigName) + } + + // Create crew manager + crewGit := git.NewGit(r.Path) + crewMgr := crew.NewManager(r, crewGit) + + bd := beads.New(beads.ResolveBeadsDir(r.Path)) + + // Get existing crew + existingCrew, err := crewMgr.List() + if err != nil { + return fmt.Errorf("listing existing crew: %w", err) + } + existingNames := make(map[string]bool) + for _, c := range existingCrew { + existingNames[c.Name] = true + } + + // Track results + var created []string + var skipped []string + var failed []string + + // Process each configured member + for _, name := range rigEntry.Crew.Members { + if existingNames[name] { + skipped = append(skipped, name) + continue + } + + if crewDryRun { + fmt.Printf("Would create: %s/%s\n", rigName, name) + created = append(created, name) + continue + } + + // Create crew workspace + fmt.Printf("Creating crew workspace %s in %s...\n", name, rigName) + + worker, err := crewMgr.Add(name, false) // No feature branch for synced crew + if err != nil { + if err == crew.ErrCrewExists { + skipped = append(skipped, name) + 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("\u2713"), rigName, name) + fmt.Printf(" Path: %s\n", worker.ClonePath) + fmt.Printf(" Branch: %s\n", worker.Branch) + + // Create agent bead for the crew worker + prefix := beads.GetPrefixForRig(townRoot, rigName) + crewID := beads.CrewBeadIDWithPrefix(prefix, 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", + } + desc := fmt.Sprintf("Crew worker %s in %s - synced from rigs.json.", 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) + fmt.Println() + } + + // Summary + if crewDryRun { + fmt.Printf("\n%s Dry run complete\n", style.Bold.Render("\u2713")) + if len(created) > 0 { + fmt.Printf(" Would create: %v\n", created) + } + if len(skipped) > 0 { + fmt.Printf(" Already exist: %v\n", skipped) + } + return nil + } + + if len(created) > 0 { + fmt.Printf("%s Created %d crew workspace(s): %v\n", + style.Bold.Render("\u2713"), len(created), created) + } + if len(skipped) > 0 { + fmt.Printf("%s Skipped %d (already exist): %v\n", + style.Dim.Render("-"), len(skipped), skipped) + } + if len(failed) > 0 { + fmt.Printf("%s Failed to create %d: %v\n", + style.Warning.Render("!"), len(failed), failed) + } + + // Show theme if configured + if rigEntry.Crew.Theme != "" { + fmt.Printf("\nCrew theme: %s\n", rigEntry.Crew.Theme) + } + + return nil +} diff --git a/internal/config/types.go b/internal/config/types.go index d3b14888..6496fc64 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -163,10 +163,11 @@ type RigsConfig struct { // RigEntry represents a single rig in the registry. type RigEntry struct { - GitURL string `json:"git_url"` - LocalRepo string `json:"local_repo,omitempty"` - AddedAt time.Time `json:"added_at"` - BeadsConfig *BeadsConfig `json:"beads,omitempty"` + GitURL string `json:"git_url"` + LocalRepo string `json:"local_repo,omitempty"` + AddedAt time.Time `json:"added_at"` + BeadsConfig *BeadsConfig `json:"beads,omitempty"` + Crew *CrewRegistryConfig `json:"crew,omitempty"` } // BeadsConfig represents beads configuration for a rig. @@ -175,6 +176,18 @@ type BeadsConfig struct { Prefix string `json:"prefix"` // issue prefix } +// CrewRegistryConfig represents crew configuration for a rig in rigs.json. +// This enables cross-machine sync of crew member definitions. +type CrewRegistryConfig struct { + // Theme selects the naming theme for crew members (e.g., "mad-max", "minerals"). + // Used when displaying crew member names and for consistency across machines. + Theme string `json:"theme,omitempty"` + + // Members lists the crew member names to create on this rig. + // Use `gt crew sync` to create missing members from this list. + Members []string `json:"members,omitempty"` +} + // CurrentTownVersion is the current schema version for TownConfig. // Version 2: Added Owner and PublicName fields for federation identity. const CurrentTownVersion = 2