package cmd import ( "bufio" "fmt" "os" "os/exec" "path/filepath" "strings" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/workspace" ) // Formula command flags var ( formulaListJSON bool formulaShowJSON bool formulaRunPR int formulaRunRig string formulaRunDryRun bool formulaCreateType string ) var formulaCmd = &cobra.Command{ Use: "formula", Aliases: []string{"formulas"}, GroupID: GroupWork, Short: "Manage workflow formulas", RunE: requireSubcommand, Long: `Manage workflow formulas - reusable molecule templates. Formulas are TOML/JSON files that define workflows with steps, variables, and composition rules. They can be "poured" to create molecules or "wisped" for ephemeral patrol cycles. Commands: list List available formulas from all search paths show Display formula details (steps, variables, composition) run Execute a formula (pour and dispatch) create Create a new formula template Search paths (in order): 1. .beads/formulas/ (project) 2. ~/.beads/formulas/ (user) 3. $GT_ROOT/.beads/formulas/ (orchestrator) Examples: gt formula list # List all formulas gt formula show shiny # Show formula details gt formula run shiny --pr=123 # Run formula on PR #123 gt formula create my-workflow # Create new formula template`, } var formulaListCmd = &cobra.Command{ Use: "list", Short: "List available formulas", Long: `List available formulas from all search paths. Searches for formula files (.formula.toml, .formula.json) in: 1. .beads/formulas/ (project) 2. ~/.beads/formulas/ (user) 3. $GT_ROOT/.beads/formulas/ (orchestrator) Examples: gt formula list # List all formulas gt formula list --json # JSON output`, RunE: runFormulaList, } var formulaShowCmd = &cobra.Command{ Use: "show ", Short: "Display formula details", Long: `Display detailed information about a formula. Shows: - Formula metadata (name, type, description) - Variables with defaults and constraints - Steps with dependencies - Composition rules (extends, aspects) Examples: gt formula show shiny gt formula show rule-of-five --json`, Args: cobra.ExactArgs(1), RunE: runFormulaShow, } var formulaRunCmd = &cobra.Command{ Use: "run ", Short: "Execute a formula", Long: `Execute a formula by pouring it and dispatching work. This command: 1. Looks up the formula by name 2. Pours it to create a molecule (or uses existing proto) 3. Dispatches the molecule to available workers For PR-based workflows, use --pr to specify the GitHub PR number. Options: --pr=N Run formula on GitHub PR #N --rig=NAME Target specific rig (default: current or gastown) --dry-run Show what would happen without executing Examples: gt formula run shiny # Run formula in current rig gt formula run shiny --pr=123 # Run on PR #123 gt formula run security-audit --rig=beads # Run in specific rig gt formula run release --dry-run # Preview execution`, Args: cobra.ExactArgs(1), RunE: runFormulaRun, } var formulaCreateCmd = &cobra.Command{ Use: "create ", Short: "Create a new formula template", Long: `Create a new formula template file. Creates a starter formula file in .beads/formulas/ with the given name. The template includes common sections that you can customize. Formula types: task Single-step task formula (default) workflow Multi-step workflow with dependencies patrol Repeating patrol cycle (for wisps) Examples: gt formula create my-task # Create task formula gt formula create my-workflow --type=workflow gt formula create nightly-check --type=patrol`, Args: cobra.ExactArgs(1), RunE: runFormulaCreate, } func init() { // List flags formulaListCmd.Flags().BoolVar(&formulaListJSON, "json", false, "Output as JSON") // Show flags formulaShowCmd.Flags().BoolVar(&formulaShowJSON, "json", false, "Output as JSON") // Run flags formulaRunCmd.Flags().IntVar(&formulaRunPR, "pr", 0, "GitHub PR number to run formula on") formulaRunCmd.Flags().StringVar(&formulaRunRig, "rig", "", "Target rig (default: current or gastown)") formulaRunCmd.Flags().BoolVar(&formulaRunDryRun, "dry-run", false, "Preview execution without running") // Create flags formulaCreateCmd.Flags().StringVar(&formulaCreateType, "type", "task", "Formula type: task, workflow, or patrol") // Add subcommands formulaCmd.AddCommand(formulaListCmd) formulaCmd.AddCommand(formulaShowCmd) formulaCmd.AddCommand(formulaRunCmd) formulaCmd.AddCommand(formulaCreateCmd) rootCmd.AddCommand(formulaCmd) } // runFormulaList delegates to bd formula list func runFormulaList(cmd *cobra.Command, args []string) error { bdArgs := []string{"formula", "list"} if formulaListJSON { bdArgs = append(bdArgs, "--json") } bdCmd := exec.Command("bd", bdArgs...) bdCmd.Stdout = os.Stdout bdCmd.Stderr = os.Stderr return bdCmd.Run() } // runFormulaShow delegates to bd formula show func runFormulaShow(cmd *cobra.Command, args []string) error { formulaName := args[0] bdArgs := []string{"formula", "show", formulaName} if formulaShowJSON { bdArgs = append(bdArgs, "--json") } bdCmd := exec.Command("bd", bdArgs...) bdCmd.Stdout = os.Stdout bdCmd.Stderr = os.Stderr return bdCmd.Run() } // runFormulaRun executes a formula func runFormulaRun(cmd *cobra.Command, args []string) error { formulaName := args[0] // Determine target rig targetRig := formulaRunRig if targetRig == "" { // Try to detect from current directory townRoot, err := workspace.FindFromCwd() if err == nil && townRoot != "" { rigName, _, rigErr := findCurrentRig(townRoot) if rigErr == nil && rigName != "" { targetRig = rigName } } if targetRig == "" { targetRig = "gastown" // Default } } if formulaRunDryRun { fmt.Printf("%s Would execute formula:\n", style.Dim.Render("[dry-run]")) fmt.Printf(" Formula: %s\n", style.Bold.Render(formulaName)) fmt.Printf(" Rig: %s\n", targetRig) if formulaRunPR > 0 { fmt.Printf(" PR: #%d\n", formulaRunPR) } return nil } // For now, provide instructions on how to execute manually // TODO: Full implementation in gt-574qn (Formula execution: Spawn convoy from formula) fmt.Printf("Formula execution is being implemented.\n\n") fmt.Printf("To run '%s' manually:\n", formulaName) fmt.Printf(" 1. View formula: bd formula show %s\n", formulaName) fmt.Printf(" 2. Cook to proto: bd cook %s\n", formulaName) fmt.Printf(" 3. Pour molecule: bd pour %s\n", formulaName) fmt.Printf(" 4. Sling to rig: gt sling %s\n", targetRig) if formulaRunPR > 0 { fmt.Printf("\n For PR #%d, set variable: --var pr=%d\n", formulaRunPR, formulaRunPR) } fmt.Printf("\n%s Full automation coming in gt-574qn\n", style.Dim.Render("Note:")) return nil } // runFormulaCreate creates a new formula template func runFormulaCreate(cmd *cobra.Command, args []string) error { formulaName := args[0] // Find or create formulas directory formulasDir := ".beads/formulas" // Check if we're in a beads-enabled directory if _, err := os.Stat(".beads"); os.IsNotExist(err) { // Try user formulas directory home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("cannot find home directory: %w", err) } formulasDir = filepath.Join(home, ".beads", "formulas") } // Ensure directory exists if err := os.MkdirAll(formulasDir, 0755); err != nil { return fmt.Errorf("creating formulas directory: %w", err) } // Generate filename filename := filepath.Join(formulasDir, formulaName+".formula.toml") // Check if file already exists if _, err := os.Stat(filename); err == nil { return fmt.Errorf("formula already exists: %s", filename) } // Generate template based on type var template string switch formulaCreateType { case "task": template = generateTaskTemplate(formulaName) case "workflow": template = generateWorkflowTemplate(formulaName) case "patrol": template = generatePatrolTemplate(formulaName) default: return fmt.Errorf("unknown formula type: %s (use: task, workflow, or patrol)", formulaCreateType) } // Write the file if err := os.WriteFile(filename, []byte(template), 0644); err != nil { return fmt.Errorf("writing formula file: %w", err) } fmt.Printf("%s Created formula: %s\n", style.Bold.Render("✓"), filename) fmt.Printf("\nNext steps:\n") fmt.Printf(" 1. Edit the formula: %s\n", filename) fmt.Printf(" 2. View it: gt formula show %s\n", formulaName) fmt.Printf(" 3. Run it: gt formula run %s\n", formulaName) return nil } func generateTaskTemplate(name string) string { // Sanitize name for use in template title := strings.ReplaceAll(name, "-", " ") title = strings.Title(title) return fmt.Sprintf(`# Formula: %s # Type: task # Created by: gt formula create description = """%s task. Add a detailed description here.""" formula = "%s" version = 1 # Single step task [[steps]] id = "do-task" title = "Execute task" description = """ Perform the main task work. **Steps:** 1. Understand the requirements 2. Implement the changes 3. Verify the work """ # Variables that can be passed when running the formula # [vars] # [vars.issue] # description = "Issue ID to work on" # required = true # # [vars.target] # description = "Target branch" # default = "main" `, name, title, name) } func generateWorkflowTemplate(name string) string { title := strings.ReplaceAll(name, "-", " ") title = strings.Title(title) return fmt.Sprintf(`# Formula: %s # Type: workflow # Created by: gt formula create description = """%s workflow. A multi-step workflow with dependencies between steps.""" formula = "%s" version = 1 # Step 1: Setup [[steps]] id = "setup" title = "Setup environment" description = """ Prepare the environment for the workflow. **Steps:** 1. Check prerequisites 2. Set up working environment """ # Step 2: Implementation (depends on setup) [[steps]] id = "implement" title = "Implement changes" needs = ["setup"] description = """ Make the necessary code changes. **Steps:** 1. Understand requirements 2. Write code 3. Test locally """ # Step 3: Test (depends on implementation) [[steps]] id = "test" title = "Run tests" needs = ["implement"] description = """ Verify the changes work correctly. **Steps:** 1. Run unit tests 2. Run integration tests 3. Check for regressions """ # Step 4: Complete (depends on tests) [[steps]] id = "complete" title = "Complete workflow" needs = ["test"] description = """ Finalize and clean up. **Steps:** 1. Commit final changes 2. Clean up temporary files """ # Variables [vars] [vars.issue] description = "Issue ID to work on" required = true `, name, title, name) } func generatePatrolTemplate(name string) string { title := strings.ReplaceAll(name, "-", " ") title = strings.Title(title) return fmt.Sprintf(`# Formula: %s # Type: patrol # Created by: gt formula create # # Patrol formulas are for repeating cycles (wisps). # They run continuously and are NOT synced to git. description = """%s patrol. A patrol formula for periodic checks. Patrol formulas create wisps (ephemeral molecules) that are NOT synced to git.""" formula = "%s" version = 1 # The patrol step(s) [[steps]] id = "check" title = "Run patrol check" description = """ Perform the patrol inspection. **Check for:** 1. Health indicators 2. Warning signs 3. Items needing attention **On findings:** - Log the issue - Escalate if critical """ # Optional: remediation step # [[steps]] # id = "remediate" # title = "Fix issues" # needs = ["check"] # description = """ # Fix any issues found during the check. # """ # Variables (optional) # [vars] # [vars.verbose] # description = "Enable verbose output" # default = "false" `, name, title, name) } // promptYesNo asks the user a yes/no question func promptYesNo(question string) bool { fmt.Printf("%s [y/N]: ", question) reader := bufio.NewReader(os.Stdin) answer, _ := reader.ReadString('\n') answer = strings.TrimSpace(strings.ToLower(answer)) return answer == "y" || answer == "yes" }