feat(cmd): Add gt formula command with subcommands (gt-gpifj)
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>
This commit is contained in:
468
internal/cmd/formula.go
Normal file
468
internal/cmd/formula.go
Normal file
@@ -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 <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"
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user