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:
nux
2026-01-01 14:52:38 -08:00
committed by Steve Yegge
parent a395b4e19b
commit 265dbcb1d0
2 changed files with 468 additions and 45 deletions

468
internal/cmd/formula.go Normal file
View 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"
}

View File

@@ -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()
}