Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions internal/cmd/formula.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
formulaRunDryRun bool
formulaRunAgent string
formulaRunFiles []string
formulaRunSet []string
formulaCreateType string
)

Expand Down Expand Up @@ -171,6 +172,7 @@ func init() {
formulaRunCmd.Flags().BoolVar(&formulaRunDryRun, "dry-run", false, "Preview execution without running")
formulaRunCmd.Flags().StringVar(&formulaRunAgent, "agent", "", "Override agent/runtime for all legs (e.g., gemini, codex, claude-haiku)")
formulaRunCmd.Flags().StringSliceVar(&formulaRunFiles, "files", nil, "Files to pass to formula legs (available as {{.files}} in templates)")
formulaRunCmd.Flags().StringSliceVar(&formulaRunSet, "set", nil, "Set input variables as key=value pairs (available as {{.key}} in templates)")

// Create flags
formulaCreateCmd.Flags().StringVar(&formulaCreateType, "type", "task", "Formula type: task, workflow, or patrol")
Expand Down Expand Up @@ -313,10 +315,22 @@ func dryRunFormula(f *formula.Formula, formulaName, targetRig string) error {
fmt.Printf(" Agent: %s\n", effectiveAgent)
}

// Show --set variables if provided
if len(formulaRunSet) > 0 {
fmt.Printf(" Set:")
for _, s := range formulaRunSet {
fmt.Printf(" %s", s)
}
fmt.Println()
}

if f.Type == formula.TypeConvoy && len(f.Legs) > 0 {
// Generate review ID for dry-run display
reviewID := generateFormulaShortID()

// Parse --set key=value pairs for template rendering
setVars := parseSetVars(formulaRunSet)

// Build target description
var targetDescription string
if formulaRunPR > 0 {
Expand Down Expand Up @@ -345,6 +359,9 @@ func dryRunFormula(f *formula.Formula, formulaName, targetRig string) error {
"review_id": reviewID,
"formula_name": formulaName,
}
for k, v := range setVars {
dirCtx[k] = v
}
outputDir = renderTemplateOrDefault(f.Output.Directory, dirCtx, ".reviews/"+reviewID)
fmt.Printf("\n Output directory: %s\n", outputDir)
}
Expand All @@ -368,6 +385,9 @@ func dryRunFormula(f *formula.Formula, formulaName, targetRig string) error {
"changed_files": changedFiles,
"files": formulaRunFiles,
}
for k, v := range setVars {
legCtx[k] = v
}
legPattern := renderTemplateOrDefault(f.Output.LegPattern, legCtx, leg.ID+"-findings.md")
outputPath := filepath.Join(outputDir, legPattern)
agentSuffix := resolveFormulaLegAgent(leg.Agent, formulaRunAgent, f.Agent)
Expand Down Expand Up @@ -402,15 +422,19 @@ func executeConvoyFormula(f *formula.Formula, formulaName, targetRig string) err
fmt.Printf("%s Executing convoy formula: %s\n\n",
style.Bold.Render("🚚"), formulaName)

// Get town beads directory for convoy creation
// Get town root and resolve rig-scoped bead prefix
townRoot, err := workspace.FindFromCwd()
if err != nil {
return fmt.Errorf("finding town root: %w", err)
}
townBeads := filepath.Join(townRoot, ".beads")

// Use rig-scoped prefix so beads match the target rig (Bug: hq- prefix
// caused gt sling to reject beads when dispatching to non-town rigs).
beadPrefix := config.GetRigPrefix(townRoot, targetRig)

// Step 1: Create convoy bead
convoyID := fmt.Sprintf("hq-cv-%s", generateFormulaShortID())
convoyID := fmt.Sprintf("%s-cv-%s", beadPrefix, generateFormulaShortID())
convoyTitle := fmt.Sprintf("%s: %s", formulaName, f.Description)
if len(convoyTitle) > 80 {
convoyTitle = convoyTitle[:77] + "..."
Expand Down Expand Up @@ -485,10 +509,13 @@ func executeConvoyFormula(f *formula.Formula, formulaName, targetRig string) err
}
}

// Parse --set key=value pairs for template rendering
setVars := parseSetVars(formulaRunSet)

// 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())
legBeadID := fmt.Sprintf("%s-leg-%s", beadPrefix, generateFormulaShortID())

// Build leg description with prompt if available
legDesc := leg.Description
Expand All @@ -511,6 +538,11 @@ func executeConvoyFormula(f *formula.Formula, formulaName, targetRig string) err
"files": formulaRunFiles,
}

// Inject --set key=value pairs into template context
for k, v := range setVars {
legCtx[k] = v
}

// Compute output path for this leg
if f.Output != nil {
legPattern := renderTemplateOrDefault(f.Output.LegPattern, legCtx, leg.ID+"-findings.md")
Expand Down Expand Up @@ -570,7 +602,7 @@ func executeConvoyFormula(f *formula.Formula, formulaName, targetRig string) err
// Step 3: Create synthesis bead if defined
var synthesisBeadID string
if f.Synthesis != nil {
synthesisBeadID = fmt.Sprintf("hq-syn-%s", generateFormulaShortID())
synthesisBeadID = fmt.Sprintf("%s-syn-%s", beadPrefix, generateFormulaShortID())

synDesc := f.Synthesis.Description
if synDesc == "" {
Expand Down Expand Up @@ -671,6 +703,17 @@ func executeConvoyFormula(f *formula.Formula, formulaName, targetRig string) err
return nil
}

// parseSetVars parses --set key=value pairs into a map for template rendering.
func parseSetVars(setArgs []string) map[string]interface{} {
vars := make(map[string]interface{})
for _, arg := range setArgs {
if idx := strings.IndexByte(arg, '='); idx > 0 {
vars[arg[:idx]] = arg[idx+1:]
}
}
return vars
}

// findFormulaFile searches for a formula file by name
func findFormulaFile(name string) (string, error) {
// Search paths in order
Expand Down