diff --git a/internal/cmd/init.go b/internal/cmd/init.go new file mode 100644 index 00000000..665c0179 --- /dev/null +++ b/internal/cmd/init.go @@ -0,0 +1,141 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/git" + "github.com/steveyegge/gastown/internal/rig" + "github.com/steveyegge/gastown/internal/style" +) + +var initForce bool + +var initCmd = &cobra.Command{ + Use: "init", + Short: "Initialize current directory as a Gas Town rig", + Long: `Initialize the current directory for use as a Gas Town rig. + +This creates the standard agent directories (polecats/, witness/, refinery/, +mayor/) and updates .git/info/exclude to ignore them. + +The current directory must be a git repository. Use --force to reinitialize +an existing rig structure.`, + RunE: runInit, +} + +func init() { + initCmd.Flags().BoolVarP(&initForce, "force", "f", false, "Reinitialize existing structure") + rootCmd.AddCommand(initCmd) +} + +func runInit(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("getting current directory: %w", err) + } + + // Check if it's a git repository + g := git.NewGit(cwd) + if _, err := g.CurrentBranch(); err != nil { + return fmt.Errorf("not a git repository (run 'git init' first)") + } + + // Check if already initialized + polecatsDir := filepath.Join(cwd, "polecats") + if _, err := os.Stat(polecatsDir); err == nil && !initForce { + return fmt.Errorf("rig already initialized (use --force to reinitialize)") + } + + fmt.Printf("%s Initializing Gas Town rig in %s\n\n", + style.Bold.Render("⚙️"), style.Dim.Render(cwd)) + + // Create agent directories + created := 0 + for _, dir := range rig.AgentDirs { + dirPath := filepath.Join(cwd, dir) + if err := os.MkdirAll(dirPath, 0755); err != nil { + return fmt.Errorf("creating %s: %w", dir, err) + } + + // Create .gitkeep to ensure directory is tracked if needed + gitkeep := filepath.Join(dirPath, ".gitkeep") + if _, err := os.Stat(gitkeep); os.IsNotExist(err) { + os.WriteFile(gitkeep, []byte(""), 0644) + } + + fmt.Printf(" ✓ Created %s/\n", dir) + created++ + } + + // Update .git/info/exclude + if err := updateGitExclude(cwd); err != nil { + fmt.Printf(" %s Could not update .git/info/exclude: %v\n", + style.Dim.Render("⚠"), err) + } else { + fmt.Printf(" ✓ Updated .git/info/exclude\n") + } + + fmt.Printf("\n%s Rig initialized with %d directories.\n", + style.Bold.Render("✓"), created) + fmt.Println() + fmt.Println("Next steps:") + fmt.Printf(" 1. Add this rig to a town: %s\n", + style.Dim.Render("gt rig add ")) + fmt.Printf(" 2. Create a polecat: %s\n", + style.Dim.Render("gt polecat add ")) + + return nil +} + +func updateGitExclude(repoPath string) error { + excludePath := filepath.Join(repoPath, ".git", "info", "exclude") + + // Ensure directory exists + excludeDir := filepath.Dir(excludePath) + if err := os.MkdirAll(excludeDir, 0755); err != nil { + return fmt.Errorf("creating .git/info: %w", err) + } + + // Read existing content + content, err := os.ReadFile(excludePath) + if err != nil && !os.IsNotExist(err) { + return err + } + + // Check if already has Gas Town section + contentStr := string(content) + if len(content) > 0 && contains(contentStr, "Gas Town") { + return nil // Already configured + } + + // Append agent dirs + additions := "\n# Gas Town agent directories\n" + for _, dir := range rig.AgentDirs { + // Get first component (e.g., "polecats" from "polecats") + // or "refinery" from "refinery/rig" + base := filepath.Dir(dir) + if base == "." { + base = dir + } + additions += base + "/\n" + } + + // Write back + return os.WriteFile(excludePath, append(content, []byte(additions)...), 0644) +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr)) +} + +func containsHelper(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go new file mode 100644 index 00000000..86929cea --- /dev/null +++ b/internal/cmd/prime.go @@ -0,0 +1,229 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/style" + "github.com/steveyegge/gastown/internal/workspace" +) + +// Role represents a detected agent role. +type Role string + +const ( + RoleMayor Role = "mayor" + RoleWitness Role = "witness" + RoleRefinery Role = "refinery" + RolePolecat Role = "polecat" + RoleUnknown Role = "unknown" +) + +var primeCmd = &cobra.Command{ + Use: "prime", + Short: "Output role context for current directory", + Long: `Detect the agent role from the current directory and output context. + +Role detection: + - Town root or mayor/rig/ → Mayor context + - /witness/rig/ → Witness context + - /refinery/rig/ → Refinery context + - /polecats// → Polecat context + +This command is typically used in shell prompts or agent initialization.`, + RunE: runPrime, +} + +func init() { + rootCmd.AddCommand(primeCmd) +} + +// RoleContext contains information about the detected role. +type RoleContext struct { + Role Role `json:"role"` + Rig string `json:"rig,omitempty"` + Polecat string `json:"polecat,omitempty"` + TownRoot string `json:"town_root"` + WorkDir string `json:"work_dir"` +} + +func runPrime(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("getting current directory: %w", err) + } + + // Find town root + townRoot, err := workspace.FindFromCwd() + if err != nil { + return fmt.Errorf("finding workspace: %w", err) + } + if townRoot == "" { + return fmt.Errorf("not in a Gas Town workspace") + } + + // Detect role + ctx := detectRole(cwd, townRoot) + + // Output context + return outputPrimeContext(ctx) +} + +func detectRole(cwd, townRoot string) RoleContext { + ctx := RoleContext{ + Role: RoleUnknown, + TownRoot: townRoot, + WorkDir: cwd, + } + + // Get relative path from town root + relPath, err := filepath.Rel(townRoot, cwd) + if err != nil { + return ctx + } + + // Normalize and split path + relPath = filepath.ToSlash(relPath) + parts := strings.Split(relPath, "/") + + // Check for mayor role + // At town root, or in mayor/ or mayor/rig/ + if relPath == "." || relPath == "" { + ctx.Role = RoleMayor + return ctx + } + if len(parts) >= 1 && parts[0] == "mayor" { + ctx.Role = RoleMayor + return ctx + } + + // At this point, first part should be a rig name + if len(parts) < 1 { + return ctx + } + rigName := parts[0] + ctx.Rig = rigName + + // Check for witness: /witness/rig/ + if len(parts) >= 2 && parts[1] == "witness" { + ctx.Role = RoleWitness + return ctx + } + + // Check for refinery: /refinery/rig/ + if len(parts) >= 2 && parts[1] == "refinery" { + ctx.Role = RoleRefinery + return ctx + } + + // Check for polecat: /polecats// + if len(parts) >= 3 && parts[1] == "polecats" { + ctx.Role = RolePolecat + ctx.Polecat = parts[2] + return ctx + } + + // Default: could be rig root - treat as unknown + return ctx +} + +func outputPrimeContext(ctx RoleContext) error { + switch ctx.Role { + case RoleMayor: + outputMayorContext(ctx) + case RoleWitness: + outputWitnessContext(ctx) + case RoleRefinery: + outputRefineryContext(ctx) + case RolePolecat: + outputPolecatContext(ctx) + default: + outputUnknownContext(ctx) + } + return nil +} + +func outputMayorContext(ctx RoleContext) { + fmt.Printf("%s\n\n", style.Bold.Render("# Mayor Context")) + fmt.Println("You are the **Mayor** - the global coordinator of Gas Town.") + fmt.Println() + fmt.Println("## Responsibilities") + fmt.Println("- Coordinate work across all rigs") + fmt.Println("- Delegate to Refineries, not directly to polecats") + fmt.Println("- Monitor overall system health") + fmt.Println() + fmt.Println("## Key Commands") + fmt.Println("- `gt status` - Show overall town status") + fmt.Println("- `gt rigs` - List all rigs") + fmt.Println("- `bd ready` - Issues ready to work") + fmt.Println() + fmt.Printf("Town root: %s\n", style.Dim.Render(ctx.TownRoot)) +} + +func outputWitnessContext(ctx RoleContext) { + fmt.Printf("%s\n\n", style.Bold.Render("# Witness Context")) + fmt.Printf("You are the **Witness** for rig: %s\n\n", style.Bold.Render(ctx.Rig)) + fmt.Println("## Responsibilities") + fmt.Println("- Monitor polecat health via heartbeat") + fmt.Println("- Spawn ephemeral agents for stuck polecats") + fmt.Println("- Report rig status to Mayor") + fmt.Println() + fmt.Println("## Key Commands") + fmt.Println("- `gt witness status` - Show witness status") + fmt.Println("- `gt polecats` - List polecats in this rig") + fmt.Println() + fmt.Printf("Rig: %s\n", style.Dim.Render(ctx.Rig)) +} + +func outputRefineryContext(ctx RoleContext) { + fmt.Printf("%s\n\n", style.Bold.Render("# Refinery Context")) + fmt.Printf("You are the **Refinery** for rig: %s\n\n", style.Bold.Render(ctx.Rig)) + fmt.Println("## Responsibilities") + fmt.Println("- Process the merge queue for this rig") + fmt.Println("- Merge polecat work to integration branch") + fmt.Println("- Resolve merge conflicts") + fmt.Println("- Land completed swarms to main") + fmt.Println() + fmt.Println("## Key Commands") + fmt.Println("- `gt merge queue` - Show pending merges") + fmt.Println("- `gt merge next` - Process next merge") + fmt.Println() + fmt.Printf("Rig: %s\n", style.Dim.Render(ctx.Rig)) +} + +func outputPolecatContext(ctx RoleContext) { + fmt.Printf("%s\n\n", style.Bold.Render("# Polecat Context")) + fmt.Printf("You are polecat **%s** in rig: %s\n\n", + style.Bold.Render(ctx.Polecat), style.Bold.Render(ctx.Rig)) + fmt.Println("## Responsibilities") + fmt.Println("- Work on assigned issues") + fmt.Println("- Commit work to your branch") + fmt.Println("- Signal completion for merge queue") + fmt.Println() + fmt.Println("## Key Commands") + fmt.Println("- `bd show ` - View your assigned issue") + fmt.Println("- `bd close ` - Mark issue complete") + fmt.Println("- `gt done` - Signal work ready for merge") + fmt.Println() + fmt.Printf("Polecat: %s | Rig: %s\n", + style.Dim.Render(ctx.Polecat), style.Dim.Render(ctx.Rig)) +} + +func outputUnknownContext(ctx RoleContext) { + fmt.Printf("%s\n\n", style.Bold.Render("# Gas Town Context")) + fmt.Println("Could not determine specific role from current directory.") + fmt.Println() + if ctx.Rig != "" { + fmt.Printf("You appear to be in rig: %s\n\n", style.Bold.Render(ctx.Rig)) + } + fmt.Println("Navigate to a specific agent directory:") + fmt.Println("- `/polecats//` - Polecat role") + fmt.Println("- `/witness/rig/` - Witness role") + fmt.Println("- `/refinery/rig/` - Refinery role") + fmt.Println("- Town root or `mayor/` - Mayor role") + fmt.Println() + fmt.Printf("Town root: %s\n", style.Dim.Render(ctx.TownRoot)) +} diff --git a/internal/cmd/status.go b/internal/cmd/status.go new file mode 100644 index 00000000..a94ab5ec --- /dev/null +++ b/internal/cmd/status.go @@ -0,0 +1,171 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/config" + "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 statusJSON bool + +var statusCmd = &cobra.Command{ + Use: "status", + Short: "Show overall town status", + Long: `Display the current status of the Gas Town workspace. + +Shows town name, registered rigs, active polecats, and witness status.`, + RunE: runStatus, +} + +func init() { + statusCmd.Flags().BoolVar(&statusJSON, "json", false, "Output as JSON") + rootCmd.AddCommand(statusCmd) +} + +// TownStatus represents the overall status of the workspace. +type TownStatus struct { + Name string `json:"name"` + Location string `json:"location"` + Rigs []RigStatus `json:"rigs"` + Summary StatusSum `json:"summary"` +} + +// RigStatus represents status of a single rig. +type RigStatus struct { + Name string `json:"name"` + Polecats []string `json:"polecats"` + PolecatCount int `json:"polecat_count"` + HasWitness bool `json:"has_witness"` + HasRefinery bool `json:"has_refinery"` +} + +// StatusSum provides summary counts. +type StatusSum struct { + RigCount int `json:"rig_count"` + PolecatCount int `json:"polecat_count"` + WitnessCount int `json:"witness_count"` + RefineryCount int `json:"refinery_count"` +} + +func runStatus(cmd *cobra.Command, args []string) error { + // Find town root + townRoot, err := workspace.FindFromCwdOrError() + if err != nil { + return fmt.Errorf("not in a Gas Town workspace: %w", err) + } + + // Load town config + townConfigPath := filepath.Join(townRoot, "config", "town.json") + townConfig, err := config.LoadTownConfig(townConfigPath) + if err != nil { + // Try to continue without config + townConfig = &config.TownConfig{Name: filepath.Base(townRoot)} + } + + // Load rigs config + rigsConfigPath := filepath.Join(townRoot, "config", "rigs.json") + rigsConfig, err := config.LoadRigsConfig(rigsConfigPath) + if err != nil { + // Empty config if file doesn't exist + rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)} + } + + // Create rig manager + g := git.NewGit(townRoot) + mgr := rig.NewManager(townRoot, rigsConfig, g) + + // Discover rigs + rigs, err := mgr.DiscoverRigs() + if err != nil { + return fmt.Errorf("discovering rigs: %w", err) + } + + // Build status + status := TownStatus{ + Name: townConfig.Name, + Location: townRoot, + Rigs: make([]RigStatus, 0, len(rigs)), + } + + for _, r := range rigs { + rs := RigStatus{ + Name: r.Name, + Polecats: r.Polecats, + PolecatCount: len(r.Polecats), + HasWitness: r.HasWitness, + HasRefinery: r.HasRefinery, + } + status.Rigs = append(status.Rigs, rs) + + // Update summary + status.Summary.PolecatCount += len(r.Polecats) + if r.HasWitness { + status.Summary.WitnessCount++ + } + if r.HasRefinery { + status.Summary.RefineryCount++ + } + } + status.Summary.RigCount = len(rigs) + + // Output + if statusJSON { + return outputStatusJSON(status) + } + return outputStatusText(status) +} + +func outputStatusJSON(status TownStatus) error { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(status) +} + +func outputStatusText(status TownStatus) error { + // Header + fmt.Printf("%s %s\n", style.Bold.Render("⚙️ Gas Town:"), status.Name) + fmt.Printf(" Location: %s\n\n", style.Dim.Render(status.Location)) + + // Summary + fmt.Printf("%s\n", style.Bold.Render("Summary")) + fmt.Printf(" Rigs: %d\n", status.Summary.RigCount) + fmt.Printf(" Polecats: %d\n", status.Summary.PolecatCount) + fmt.Printf(" Witnesses: %d\n", status.Summary.WitnessCount) + fmt.Printf(" Refineries: %d\n", status.Summary.RefineryCount) + + if len(status.Rigs) == 0 { + fmt.Printf("\n%s\n", style.Dim.Render("No rigs registered. Use 'gt rig add' to add one.")) + return nil + } + + // Rigs detail + fmt.Printf("\n%s\n", style.Bold.Render("Rigs")) + for _, r := range status.Rigs { + // Rig name with indicators + indicators := "" + if r.HasWitness { + indicators += " 👁" + } + if r.HasRefinery { + indicators += " 🏭" + } + + fmt.Printf(" %s%s\n", style.Bold.Render(r.Name), indicators) + + if len(r.Polecats) > 0 { + fmt.Printf(" Polecats: %v\n", r.Polecats) + } else { + fmt.Printf(" %s\n", style.Dim.Render("No polecats")) + } + } + + return nil +}