diff --git a/internal/cmd/formula.go b/internal/cmd/formula.go new file mode 100644 index 00000000..7fe6f8b4 --- /dev/null +++ b/internal/cmd/formula.go @@ -0,0 +1,468 @@ +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" +} diff --git a/internal/cmd/formulas.go b/internal/cmd/formulas.go deleted file mode 100644 index baa346cd..00000000 --- a/internal/cmd/formulas.go +++ /dev/null @@ -1,45 +0,0 @@ -package cmd - -import ( - "os" - "os/exec" - - "github.com/spf13/cobra" -) - -var formulasCmd = &cobra.Command{ - Use: "formulas", - Aliases: []string{"formula"}, - GroupID: GroupWork, - Short: "List available workflow formulas", - Long: `List available workflow formulas (molecule templates). - -Formulas are reusable workflow templates that can be instantiated via: - gt sling mol-xxx target # Pour formula and dispatch - -This is a convenience alias for 'bd formula list'. - -Examples: - gt formulas # List all formulas - gt formulas --json # JSON output`, - RunE: runFormulas, -} - -var formulasJSON bool - -func init() { - formulasCmd.Flags().BoolVar(&formulasJSON, "json", false, "Output as JSON") - rootCmd.AddCommand(formulasCmd) -} - -func runFormulas(cmd *cobra.Command, args []string) error { - bdArgs := []string{"formula", "list"} - if formulasJSON { - bdArgs = append(bdArgs, "--json") - } - - bdCmd := exec.Command("bd", bdArgs...) - bdCmd.Stdout = os.Stdout - bdCmd.Stderr = os.Stderr - return bdCmd.Run() -}