// Package cmd provides CLI commands for the gt tool. package cmd import ( "fmt" "os" "path/filepath" "time" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/polecat" "github.com/steveyegge/gastown/internal/refinery" "github.com/steveyegge/gastown/internal/rig" "github.com/steveyegge/gastown/internal/session" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/witness" "github.com/steveyegge/gastown/internal/workspace" ) var rigCmd = &cobra.Command{ Use: "rig", Short: "Manage rigs in the workspace", Long: `Manage rigs (project containers) in the Gas Town workspace. A rig is a container for managing a project and its agents: - refinery/rig/ Canonical main clone (Refinery's working copy) - mayor/rig/ Mayor's working clone for this rig - crew// Human workspace(s) - witness/ Witness agent (no clone) - polecats/ Worker directories - .beads/ Rig-level issue tracking`, } var rigAddCmd = &cobra.Command{ Use: "add ", Short: "Add a new rig to the workspace", Long: `Add a new rig by cloning a repository. This creates a rig container with: - config.json Rig configuration - .beads/ Rig-level issue tracking (initialized) - refinery/rig/ Canonical main clone - mayor/rig/ Mayor's working clone - crew/main/ Default human workspace - witness/ Witness agent directory - polecats/ Worker directory (empty) Example: gt rig add gastown https://github.com/steveyegge/gastown gt rig add my-project git@github.com:user/repo.git --prefix mp`, Args: cobra.ExactArgs(2), RunE: runRigAdd, } var rigListCmd = &cobra.Command{ Use: "list", Short: "List all rigs in the workspace", RunE: runRigList, } var rigRemoveCmd = &cobra.Command{ Use: "remove ", Short: "Remove a rig from the registry (does not delete files)", Args: cobra.ExactArgs(1), RunE: runRigRemove, } var rigResetCmd = &cobra.Command{ Use: "reset", Short: "Reset rig state (handoff content, mail, etc.)", Long: `Reset various rig state. By default, resets all resettable state. Use flags to reset specific items. Examples: gt rig reset # Reset all state gt rig reset --handoff # Clear handoff content only gt rig reset --mail # Clear stale mail messages only`, RunE: runRigReset, } var rigShutdownCmd = &cobra.Command{ Use: "shutdown ", Short: "Gracefully stop all rig agents", Long: `Stop all agents in a rig. This command gracefully shuts down: - All polecat sessions - The refinery (if running) - The witness (if running) Before shutdown, checks all polecats for uncommitted work: - Uncommitted changes (modified/untracked files) - Stashes - Unpushed commits Use --force to skip graceful shutdown and kill immediately. Use --nuclear to bypass ALL safety checks (will lose work!). Examples: gt rig shutdown gastown gt rig shutdown gastown --force gt rig shutdown gastown --nuclear # DANGER: loses uncommitted work`, Args: cobra.ExactArgs(1), RunE: runRigShutdown, } // Flags var ( rigAddPrefix string rigAddCrew string rigResetHandoff bool rigResetMail bool rigResetRole string rigShutdownForce bool rigShutdownNuclear bool ) func init() { rootCmd.AddCommand(rigCmd) rigCmd.AddCommand(rigAddCmd) rigCmd.AddCommand(rigListCmd) rigCmd.AddCommand(rigRemoveCmd) rigCmd.AddCommand(rigResetCmd) rigCmd.AddCommand(rigShutdownCmd) rigAddCmd.Flags().StringVar(&rigAddPrefix, "prefix", "", "Beads issue prefix (default: derived from name)") rigAddCmd.Flags().StringVar(&rigAddCrew, "crew", "main", "Default crew workspace name") rigResetCmd.Flags().BoolVar(&rigResetHandoff, "handoff", false, "Clear handoff content") rigResetCmd.Flags().BoolVar(&rigResetMail, "mail", false, "Clear stale mail messages") rigResetCmd.Flags().StringVar(&rigResetRole, "role", "", "Role to reset (default: auto-detect from cwd)") rigShutdownCmd.Flags().BoolVarP(&rigShutdownForce, "force", "f", false, "Force immediate shutdown") rigShutdownCmd.Flags().BoolVar(&rigShutdownNuclear, "nuclear", false, "DANGER: Bypass ALL safety checks (loses uncommitted work!)") } func runRigAdd(cmd *cobra.Command, args []string) error { name := args[0] gitURL := args[1] // Find workspace townRoot, err := workspace.FindFromCwdOrError() if err != nil { return fmt.Errorf("not in a Gas Town workspace: %w", err) } // Load rigs config rigsPath := filepath.Join(townRoot, "mayor", "rigs.json") rigsConfig, err := config.LoadRigsConfig(rigsPath) if err != nil { // Create new if doesn't exist rigsConfig = &config.RigsConfig{ Version: 1, Rigs: make(map[string]config.RigEntry), } } // Create rig manager g := git.NewGit(townRoot) mgr := rig.NewManager(townRoot, rigsConfig, g) fmt.Printf("Creating rig %s...\n", style.Bold.Render(name)) fmt.Printf(" Repository: %s\n", gitURL) startTime := time.Now() // Add the rig newRig, err := mgr.AddRig(rig.AddRigOptions{ Name: name, GitURL: gitURL, BeadsPrefix: rigAddPrefix, CrewName: rigAddCrew, }) if err != nil { return fmt.Errorf("adding rig: %w", err) } // Save updated rigs config if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil { return fmt.Errorf("saving rigs config: %w", err) } elapsed := time.Since(startTime) fmt.Printf("\n%s Rig created in %.1fs\n", style.Success.Render("✓"), elapsed.Seconds()) fmt.Printf("\nStructure:\n") fmt.Printf(" %s/\n", name) fmt.Printf(" ├── config.json\n") fmt.Printf(" ├── .beads/ (prefix: %s)\n", newRig.Config.Prefix) fmt.Printf(" ├── refinery/rig/ (canonical main)\n") fmt.Printf(" ├── mayor/rig/ (mayor's clone)\n") fmt.Printf(" ├── crew/%s/ (your workspace)\n", rigAddCrew) fmt.Printf(" ├── witness/\n") fmt.Printf(" └── polecats/\n") fmt.Printf("\nNext steps:\n") fmt.Printf(" cd %s/crew/%s # Work in your clone\n", filepath.Join(townRoot, name), rigAddCrew) fmt.Printf(" bd ready # See available work\n") return nil } func runRigList(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 rigsPath := filepath.Join(townRoot, "mayor", "rigs.json") rigsConfig, err := config.LoadRigsConfig(rigsPath) if err != nil { fmt.Println("No rigs configured.") return nil } if len(rigsConfig.Rigs) == 0 { fmt.Println("No rigs configured.") fmt.Printf("\nAdd one with: %s\n", style.Dim.Render("gt rig add ")) return nil } // Create rig manager to get details g := git.NewGit(townRoot) mgr := rig.NewManager(townRoot, rigsConfig, g) fmt.Printf("Rigs in %s:\n\n", townRoot) for name := range rigsConfig.Rigs { r, err := mgr.GetRig(name) if err != nil { fmt.Printf(" %s %s\n", style.Warning.Render("!"), name) continue } summary := r.Summary() fmt.Printf(" %s\n", style.Bold.Render(name)) fmt.Printf(" Polecats: %d Crew: %d\n", summary.PolecatCount, summary.CrewCount) agents := []string{} if summary.HasRefinery { agents = append(agents, "refinery") } if summary.HasWitness { agents = append(agents, "witness") } if r.HasMayor { agents = append(agents, "mayor") } if len(agents) > 0 { fmt.Printf(" Agents: %v\n", agents) } fmt.Println() } return nil } func runRigRemove(cmd *cobra.Command, args []string) error { name := args[0] // Find workspace townRoot, err := workspace.FindFromCwdOrError() if err != nil { return fmt.Errorf("not in a Gas Town workspace: %w", err) } // Load rigs config rigsPath := filepath.Join(townRoot, "mayor", "rigs.json") rigsConfig, err := config.LoadRigsConfig(rigsPath) if err != nil { return fmt.Errorf("loading rigs config: %w", err) } // Create rig manager g := git.NewGit(townRoot) mgr := rig.NewManager(townRoot, rigsConfig, g) if err := mgr.RemoveRig(name); err != nil { return fmt.Errorf("removing rig: %w", err) } // Save updated config if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil { return fmt.Errorf("saving rigs config: %w", err) } fmt.Printf("%s Rig %s removed from registry\n", style.Success.Render("✓"), name) fmt.Printf("\nNote: Files at %s were NOT deleted.\n", filepath.Join(townRoot, name)) fmt.Printf("To delete: %s\n", style.Dim.Render(fmt.Sprintf("rm -rf %s", filepath.Join(townRoot, name)))) return nil } func runRigReset(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) } cwd, err := os.Getwd() if err != nil { return fmt.Errorf("getting current directory: %w", err) } // Determine role to reset roleKey := rigResetRole if roleKey == "" { // Auto-detect from cwd ctx := detectRole(cwd, townRoot) if ctx.Role == RoleUnknown { return fmt.Errorf("could not detect role from current directory; use --role to specify") } roleKey = string(ctx.Role) } // If no specific flags, reset all; otherwise only reset what's specified resetAll := !rigResetHandoff && !rigResetMail bd := beads.New(townRoot) // Reset handoff content if resetAll || rigResetHandoff { if err := bd.ClearHandoffContent(roleKey); err != nil { return fmt.Errorf("clearing handoff content: %w", err) } fmt.Printf("%s Cleared handoff content for %s\n", style.Success.Render("✓"), roleKey) } // Clear stale mail messages if resetAll || rigResetMail { result, err := bd.ClearMail("Cleared during reset") if err != nil { return fmt.Errorf("clearing mail: %w", err) } if result.Closed > 0 || result.Cleared > 0 { fmt.Printf("%s Cleared mail: %d closed, %d pinned cleared\n", style.Success.Render("✓"), result.Closed, result.Cleared) } else { fmt.Printf("%s No mail to clear\n", style.Success.Render("✓")) } } return nil } // Helper to check if path exists func pathExists(path string) bool { _, err := os.Stat(path) return err == nil } func runRigShutdown(cmd *cobra.Command, args []string) error { rigName := args[0] // Find workspace townRoot, err := workspace.FindFromCwdOrError() if err != nil { return fmt.Errorf("not in a Gas Town workspace: %w", err) } // Load rigs config and get rig rigsPath := filepath.Join(townRoot, "mayor", "rigs.json") rigsConfig, err := config.LoadRigsConfig(rigsPath) if err != nil { rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)} } 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) } // Check all polecats for uncommitted work (unless nuclear) if !rigShutdownNuclear { polecatGit := git.NewGit(r.Path) polecatMgr := polecat.NewManager(r, polecatGit) polecats, err := polecatMgr.List() if err == nil && len(polecats) > 0 { var problemPolecats []struct { name string status *git.UncommittedWorkStatus } for _, p := range polecats { pGit := git.NewGit(p.ClonePath) status, err := pGit.CheckUncommittedWork() if err == nil && !status.Clean() { problemPolecats = append(problemPolecats, struct { name string status *git.UncommittedWorkStatus }{p.Name, status}) } } if len(problemPolecats) > 0 { fmt.Printf("\n%s Cannot shutdown - polecats have uncommitted work:\n\n", style.Warning.Render("⚠")) for _, pp := range problemPolecats { fmt.Printf(" %s: %s\n", style.Bold.Render(pp.name), pp.status.String()) } fmt.Printf("\nUse %s to force shutdown (DANGER: will lose work!)\n", style.Bold.Render("--nuclear")) return fmt.Errorf("refusing to shutdown with uncommitted work") } } } fmt.Printf("Shutting down rig %s...\n", style.Bold.Render(rigName)) var errors []string // 1. Stop all polecat sessions t := tmux.NewTmux() sessMgr := session.NewManager(t, r) infos, err := sessMgr.List() if err == nil && len(infos) > 0 { fmt.Printf(" Stopping %d polecat session(s)...\n", len(infos)) if err := sessMgr.StopAll(rigShutdownForce); err != nil { errors = append(errors, fmt.Sprintf("polecat sessions: %v", err)) } } // 2. Stop the refinery refMgr := refinery.NewManager(r) refStatus, err := refMgr.Status() if err == nil && refStatus.State == refinery.StateRunning { fmt.Printf(" Stopping refinery...\n") if err := refMgr.Stop(); err != nil { errors = append(errors, fmt.Sprintf("refinery: %v", err)) } } // 3. Stop the witness witMgr := witness.NewManager(r) witStatus, err := witMgr.Status() if err == nil && witStatus.State == witness.StateRunning { fmt.Printf(" Stopping witness...\n") if err := witMgr.Stop(); err != nil { errors = append(errors, fmt.Sprintf("witness: %v", err)) } } if len(errors) > 0 { fmt.Printf("\n%s Some agents failed to stop:\n", style.Warning.Render("⚠")) for _, e := range errors { fmt.Printf(" - %s\n", e) } return fmt.Errorf("shutdown incomplete") } fmt.Printf("%s Rig %s shut down successfully\n", style.Success.Render("✓"), rigName) return nil }