Add a proper `gt formula` command with subcommands: - gt formula list - List available formulas - gt formula show <name> - Display formula details - gt formula run <name> - Execute a formula (scaffold for gt-574qn) - gt formula create <name> - Create new formula templates The create command generates valid TOML templates for: - task: Single-step task formula - workflow: Multi-step workflow with dependencies - patrol: Repeating patrol cycle for wisps Replaces the simple formulas.go that only delegated to `bd formula list`. The `formulas` alias is preserved for backwards compatibility. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
469 lines
12 KiB
Go
469 lines
12 KiB
Go
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 <name>",
|
|
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 <name>",
|
|
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 <name>",
|
|
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 <mol-id> %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"
|
|
}
|