Files
gastown/internal/cmd/formula.go
coma a1f843c11d fix(convoy): ensure custom types before convoy creation
Add EnsureCustomTypes call to createAutoConvoy (sling) and
executeConvoyFormula (formula) to ensure the 'convoy' type is
registered before attempting to create convoy beads.

This fixes "validation failed: invalid issue type: convoy" errors
that occurred when convoy creation was attempted before custom types
were configured in the beads database.

Fixes: hq-ledua

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 12:37:49 -08:00

971 lines
26 KiB
Go

package cmd
import (
"bufio"
"crypto/rand"
"encoding/base32"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// 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 (or uses default from rig config)
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.
If no formula name is provided, uses the default formula configured in
the rig's settings/config.json under workflow.default_formula.
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 # Run default formula from rig config
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.MaximumNArgs(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 by spawning a convoy of polecats.
// For convoy-type formulas, it creates a convoy bead, creates leg beads,
// and slings each leg to a separate polecat with leg-specific prompts.
func runFormulaRun(cmd *cobra.Command, args []string) error {
// Determine target rig first (needed for default formula lookup)
targetRig := formulaRunRig
var rigPath string
if targetRig == "" {
// Try to detect from current directory
townRoot, err := workspace.FindFromCwd()
if err == nil && townRoot != "" {
rigName, r, rigErr := findCurrentRig(townRoot)
if rigErr == nil && rigName != "" {
targetRig = rigName
if r != nil {
rigPath = r.Path
}
}
// If we still don't have a target rig but have townRoot, use gastown
if targetRig == "" {
targetRig = "gastown"
rigPath = filepath.Join(townRoot, "gastown")
}
} else {
// No town root found, fall back to gastown without rigPath
targetRig = "gastown"
}
} else {
// If rig specified, construct path
townRoot, err := workspace.FindFromCwd()
if err == nil && townRoot != "" {
rigPath = filepath.Join(townRoot, targetRig)
}
}
// Get formula name from args or default
var formulaName string
if len(args) > 0 {
formulaName = args[0]
} else {
// Try to get default formula from rig config
if rigPath != "" {
formulaName = config.GetDefaultFormula(rigPath)
}
if formulaName == "" {
return fmt.Errorf("no formula specified and no default formula configured\n\nTo set a default formula, add to your rig's settings/config.json:\n \"workflow\": {\n \"default_formula\": \"<formula-name>\"\n }")
}
fmt.Printf("%s Using default formula: %s\n", style.Dim.Render("Note:"), formulaName)
}
// Find the formula file
formulaPath, err := findFormulaFile(formulaName)
if err != nil {
return fmt.Errorf("finding formula: %w", err)
}
// Parse the formula
f, err := parseFormulaFile(formulaPath)
if err != nil {
return fmt.Errorf("parsing formula: %w", err)
}
// Handle dry-run mode
if formulaRunDryRun {
return dryRunFormula(f, formulaName, targetRig)
}
// Currently only convoy formulas are supported for execution
if f.Type != "convoy" {
fmt.Printf("%s Formula type '%s' not yet supported for execution.\n",
style.Dim.Render("Note:"), f.Type)
fmt.Printf("Currently only 'convoy' formulas can be run.\n")
fmt.Printf("\nTo run '%s' manually:\n", formulaName)
fmt.Printf(" 1. View formula: gt 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)
return nil
}
// Execute convoy formula
return executeConvoyFormula(f, formulaName, targetRig)
}
// dryRunFormula shows what would happen without executing
func dryRunFormula(f *formulaData, formulaName, targetRig string) error {
fmt.Printf("%s Would execute formula:\n", style.Dim.Render("[dry-run]"))
fmt.Printf(" Formula: %s\n", style.Bold.Render(formulaName))
fmt.Printf(" Type: %s\n", f.Type)
fmt.Printf(" Rig: %s\n", targetRig)
if formulaRunPR > 0 {
fmt.Printf(" PR: #%d\n", formulaRunPR)
}
if f.Type == "convoy" && len(f.Legs) > 0 {
fmt.Printf("\n Legs (%d parallel):\n", len(f.Legs))
for _, leg := range f.Legs {
fmt.Printf(" • %s: %s\n", leg.ID, leg.Title)
}
if f.Synthesis != nil {
fmt.Printf("\n Synthesis:\n")
fmt.Printf(" • %s\n", f.Synthesis.Title)
}
}
return nil
}
// executeConvoyFormula spawns a convoy of polecats to execute a convoy formula
func executeConvoyFormula(f *formulaData, formulaName, targetRig string) error {
fmt.Printf("%s Executing convoy formula: %s\n\n",
style.Bold.Render("🚚"), formulaName)
// Get town beads directory for convoy creation
townRoot, err := workspace.FindFromCwd()
if err != nil {
return fmt.Errorf("finding town root: %w", err)
}
townBeads := filepath.Join(townRoot, ".beads")
// Ensure custom types (including 'convoy') are registered in town beads.
// This handles cases where install didn't complete or beads was initialized manually.
if err := beads.EnsureCustomTypes(townBeads); err != nil {
return fmt.Errorf("ensuring custom types: %w", err)
}
// Step 1: Create convoy bead
convoyID := fmt.Sprintf("hq-cv-%s", generateFormulaShortID())
convoyTitle := fmt.Sprintf("%s: %s", formulaName, f.Description)
if len(convoyTitle) > 80 {
convoyTitle = convoyTitle[:77] + "..."
}
// Build description with formula context
description := fmt.Sprintf("Formula convoy: %s\n\nLegs: %d\nRig: %s",
formulaName, len(f.Legs), targetRig)
if formulaRunPR > 0 {
description += fmt.Sprintf("\nPR: #%d", formulaRunPR)
}
createArgs := []string{
"create",
"--type=convoy",
"--id=" + convoyID,
"--title=" + convoyTitle,
"--description=" + description,
}
if beads.NeedsForceForID(convoyID) {
createArgs = append(createArgs, "--force")
}
createCmd := exec.Command("bd", createArgs...)
createCmd.Dir = townBeads
createCmd.Stderr = os.Stderr
if err := createCmd.Run(); err != nil {
return fmt.Errorf("creating convoy bead: %w", err)
}
fmt.Printf("%s Created convoy: %s\n", style.Bold.Render("✓"), convoyID)
// Step 2: Create leg beads and track them
legBeads := make(map[string]string) // leg.ID -> bead ID
for _, leg := range f.Legs {
legBeadID := fmt.Sprintf("hq-leg-%s", generateFormulaShortID())
// Build leg description with prompt if available
legDesc := leg.Description
if f.Prompts != nil {
if basePrompt, ok := f.Prompts["base"]; ok {
legDesc = fmt.Sprintf("%s\n\n---\nBase Prompt:\n%s", leg.Description, basePrompt)
}
}
legArgs := []string{
"create",
"--type=task",
"--id=" + legBeadID,
"--title=" + leg.Title,
"--description=" + legDesc,
}
if beads.NeedsForceForID(legBeadID) {
legArgs = append(legArgs, "--force")
}
legCmd := exec.Command("bd", legArgs...)
legCmd.Dir = townBeads
legCmd.Stderr = os.Stderr
if err := legCmd.Run(); err != nil {
fmt.Printf("%s Failed to create leg bead for %s: %v\n",
style.Dim.Render("Warning:"), leg.ID, err)
continue
}
// Track the leg with the convoy
trackArgs := []string{"dep", "add", convoyID, legBeadID, "--type=tracks"}
trackCmd := exec.Command("bd", trackArgs...)
trackCmd.Dir = townBeads
if err := trackCmd.Run(); err != nil {
fmt.Printf("%s Failed to track leg %s: %v\n",
style.Dim.Render("Warning:"), leg.ID, err)
}
legBeads[leg.ID] = legBeadID
fmt.Printf(" %s Created leg: %s (%s)\n", style.Dim.Render("○"), leg.ID, legBeadID)
}
// Step 3: Create synthesis bead if defined
var synthesisBeadID string
if f.Synthesis != nil {
synthesisBeadID = fmt.Sprintf("hq-syn-%s", generateFormulaShortID())
synDesc := f.Synthesis.Description
if synDesc == "" {
synDesc = "Synthesize findings from all legs into unified output"
}
synArgs := []string{
"create",
"--type=task",
"--id=" + synthesisBeadID,
"--title=" + f.Synthesis.Title,
"--description=" + synDesc,
}
if beads.NeedsForceForID(synthesisBeadID) {
synArgs = append(synArgs, "--force")
}
synCmd := exec.Command("bd", synArgs...)
synCmd.Dir = townBeads
synCmd.Stderr = os.Stderr
if err := synCmd.Run(); err != nil {
fmt.Printf("%s Failed to create synthesis bead: %v\n",
style.Dim.Render("Warning:"), err)
} else {
// Track synthesis with convoy
trackArgs := []string{"dep", "add", convoyID, synthesisBeadID, "--type=tracks"}
trackCmd := exec.Command("bd", trackArgs...)
trackCmd.Dir = townBeads
_ = trackCmd.Run()
// Add dependencies: synthesis depends on all legs
for _, legBeadID := range legBeads {
depArgs := []string{"dep", "add", synthesisBeadID, legBeadID}
depCmd := exec.Command("bd", depArgs...)
depCmd.Dir = townBeads
_ = depCmd.Run()
}
fmt.Printf(" %s Created synthesis: %s\n", style.Dim.Render("★"), synthesisBeadID)
}
}
// Step 4: Sling each leg to a polecat
fmt.Printf("\n%s Dispatching legs to polecats...\n\n", style.Bold.Render("→"))
slingCount := 0
for _, leg := range f.Legs {
legBeadID, ok := legBeads[leg.ID]
if !ok {
continue
}
// Build context message for the polecat
contextMsg := fmt.Sprintf("Convoy leg: %s\nFocus: %s", leg.Title, leg.Focus)
// Use gt sling with args for leg-specific context
slingArgs := []string{
"sling", legBeadID, targetRig,
"-a", leg.Description,
"-s", leg.Title,
}
slingCmd := exec.Command("gt", slingArgs...)
slingCmd.Stdout = os.Stdout
slingCmd.Stderr = os.Stderr
if err := slingCmd.Run(); err != nil {
fmt.Printf("%s Failed to sling leg %s: %v\n",
style.Dim.Render("Warning:"), leg.ID, err)
// Add comment to bead about failure
commentArgs := []string{"comment", legBeadID, fmt.Sprintf("Failed to sling: %v", err)}
commentCmd := exec.Command("bd", commentArgs...)
commentCmd.Dir = townBeads
_ = commentCmd.Run()
continue
}
slingCount++
_ = contextMsg // Used in future for richer context
}
// Summary
fmt.Printf("\n%s Convoy dispatched!\n", style.Bold.Render("✓"))
fmt.Printf(" Convoy: %s\n", convoyID)
fmt.Printf(" Legs: %d dispatched\n", slingCount)
if synthesisBeadID != "" {
fmt.Printf(" Synthesis: %s (blocked until legs complete)\n", synthesisBeadID)
}
fmt.Printf("\n Track progress: gt convoy status %s\n", convoyID)
return nil
}
// formulaData holds parsed formula information
type formulaData struct {
Name string
Description string
Type string
Legs []formulaLeg
Synthesis *formulaSynthesis
Prompts map[string]string
}
type formulaLeg struct {
ID string
Title string
Focus string
Description string
}
type formulaSynthesis struct {
Title string
Description string
DependsOn []string
}
// findFormulaFile searches for a formula file by name
func findFormulaFile(name string) (string, error) {
// Search paths in order
searchPaths := []string{}
// 1. Project .beads/formulas/
if cwd, err := os.Getwd(); err == nil {
searchPaths = append(searchPaths, filepath.Join(cwd, ".beads", "formulas"))
}
// 2. Town .beads/formulas/
if townRoot, err := workspace.FindFromCwd(); err == nil {
searchPaths = append(searchPaths, filepath.Join(townRoot, ".beads", "formulas"))
}
// 3. User ~/.beads/formulas/
if home, err := os.UserHomeDir(); err == nil {
searchPaths = append(searchPaths, filepath.Join(home, ".beads", "formulas"))
}
// Try each path with common extensions
extensions := []string{".formula.toml", ".formula.json"}
for _, basePath := range searchPaths {
for _, ext := range extensions {
path := filepath.Join(basePath, name+ext)
if _, err := os.Stat(path); err == nil {
return path, nil
}
}
}
return "", fmt.Errorf("formula '%s' not found in search paths", name)
}
// parseFormulaFile parses a formula file into formulaData
func parseFormulaFile(path string) (*formulaData, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Use simple TOML parsing for the fields we need
// (avoids importing the full formula package which might cause cycles)
f := &formulaData{
Prompts: make(map[string]string),
}
content := string(data)
// Parse formula name
if match := extractTOMLValue(content, "formula"); match != "" {
f.Name = match
}
// Parse description
if match := extractTOMLMultiline(content, "description"); match != "" {
f.Description = match
}
// Parse type
if match := extractTOMLValue(content, "type"); match != "" {
f.Type = match
}
// Parse legs (convoy formulas)
f.Legs = extractLegs(content)
// Parse synthesis
f.Synthesis = extractSynthesis(content)
// Parse prompts
f.Prompts = extractPrompts(content)
return f, nil
}
// extractTOMLValue extracts a simple quoted value from TOML
func extractTOMLValue(content, key string) string {
// Match: key = "value" or key = 'value'
for _, line := range strings.Split(content, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, key+" =") || strings.HasPrefix(line, key+"=") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
val := strings.TrimSpace(parts[1])
// Remove quotes
if len(val) >= 2 && (val[0] == '"' || val[0] == '\'') {
return val[1 : len(val)-1]
}
return val
}
}
}
return ""
}
// extractTOMLMultiline extracts a multiline string (""" ... """)
func extractTOMLMultiline(content, key string) string {
// Look for key = """
keyPattern := key + ` = """`
idx := strings.Index(content, keyPattern)
if idx == -1 {
// Try single-line
return extractTOMLValue(content, key)
}
start := idx + len(keyPattern)
end := strings.Index(content[start:], `"""`)
if end == -1 {
return ""
}
return strings.TrimSpace(content[start : start+end])
}
// extractLegs parses [[legs]] sections from TOML
func extractLegs(content string) []formulaLeg {
var legs []formulaLeg
// Split by [[legs]]
sections := strings.Split(content, "[[legs]]")
for i, section := range sections {
if i == 0 {
continue // Skip content before first [[legs]]
}
// Find where this section ends (next [[ or EOF)
endIdx := strings.Index(section, "[[")
if endIdx == -1 {
endIdx = len(section)
}
section = section[:endIdx]
leg := formulaLeg{
ID: extractTOMLValue(section, "id"),
Title: extractTOMLValue(section, "title"),
Focus: extractTOMLValue(section, "focus"),
Description: extractTOMLMultiline(section, "description"),
}
if leg.ID != "" {
legs = append(legs, leg)
}
}
return legs
}
// extractSynthesis parses [synthesis] section from TOML
func extractSynthesis(content string) *formulaSynthesis {
idx := strings.Index(content, "[synthesis]")
if idx == -1 {
return nil
}
section := content[idx:]
// Find where section ends
if endIdx := strings.Index(section[1:], "\n["); endIdx != -1 {
section = section[:endIdx+1]
}
syn := &formulaSynthesis{
Title: extractTOMLValue(section, "title"),
Description: extractTOMLMultiline(section, "description"),
}
// Parse depends_on array
if depsLine := extractTOMLValue(section, "depends_on"); depsLine != "" {
// Simple array parsing: ["a", "b", "c"]
depsLine = strings.Trim(depsLine, "[]")
for _, dep := range strings.Split(depsLine, ",") {
dep = strings.Trim(strings.TrimSpace(dep), `"'`)
if dep != "" {
syn.DependsOn = append(syn.DependsOn, dep)
}
}
}
if syn.Title == "" && syn.Description == "" {
return nil
}
return syn
}
// extractPrompts parses [prompts] section from TOML
func extractPrompts(content string) map[string]string {
prompts := make(map[string]string)
idx := strings.Index(content, "[prompts]")
if idx == -1 {
return prompts
}
section := content[idx:]
// Find where section ends
if endIdx := strings.Index(section[1:], "\n["); endIdx != -1 {
section = section[:endIdx+1]
}
// Extract base prompt
if base := extractTOMLMultiline(section, "base"); base != "" {
prompts["base"] = base
}
return prompts
}
// generateFormulaShortID generates a short random ID (5 lowercase chars)
func generateFormulaShortID() string {
b := make([]byte, 3)
_, _ = rand.Read(b)
return strings.ToLower(base32.StdEncoding.EncodeToString(b)[:5])
}
// 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 = cases.Title(language.English).String(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 = cases.Title(language.English).String(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 = cases.Title(language.English).String(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"
}