feat(recipe): add recipe system for Result[T] refactoring

Implement recipe loading, parsing, and running infrastructure.
Add initial result-refactor recipe for converting Go error handling to monadic Result[T] style.
Includes markdown recipe definition, YAML frontmatter parsing, step extraction, and basic runner.
This commit is contained in:
Greg Gauthier 2026-03-06 18:35:58 +00:00
parent 736fe4fcd6
commit aa38e92fb5
4 changed files with 184 additions and 0 deletions

View File

@ -0,0 +1,54 @@
---
name: result-refactor
description: Convert traditional Go error handling to Result[T] monadic style
version: 1.0
parameters:
package_path:
type: string
default: internal/service
description: Package to refactor
dry_run:
type: bool
default: true
description: If true, only generate patches
allowed_shell_commands:
- go test ./...
- go fmt ./...
- go vet ./...
- rg --files
- git diff --name-only
- jq
---
# Result[T] Refactoring Recipe
**Overview**
Refactors all error handling in the target package to use the new Result[T] pattern.
## Execution Steps
### Step 1: Discover files
**Objective:** Find every file that needs changing.
**Instructions:** Recursively scan `{{.package_path}}` for `.go` files containing `if err != nil`.
**Expected output:** A clean numbered list of full file paths (one per line).
### Step 2: Refactor each file
**Objective:** Generate the updated code.
**Instructions:** For each file from Step 1:
- Read the full original content.
- Refactor it to use `Result[T]` instead of naked errors (follow existing style, preserve comments).
- Return *ONLY* the complete new file inside a ```go code block (no explanations).
**Expected output:** One ```go block per file, clearly labelled with the filename.
### Step 3: Apply or patch
**Objective:** Safely write changes or create reviewable output.
**Instructions:**
- If `dry_run` is true → create a unified diff patch file for review.
- If false → write the new files (backup originals as `.bak`).
**Expected output:** Confirmation of what was written + full path to any patch file.
### Final Summary
Give me a concise executive summary: number of files changed, any warnings or patterns you noticed, and your recommended next step.

82
internal/recipe/loader.go Normal file
View File

@ -0,0 +1,82 @@
package recipe
import (
"bytes"
"fmt"
"os"
"regexp"
"strings"
"text/template"
"gopkg.in/yaml.v3"
)
var (
stepRe = regexp.MustCompile(`(?m)^### Step (\d+): (.+)$`)
subRe = regexp.MustCompile(`(?m)^(\*\*Objective:\*\*|\*\*Instructions:\*\*|\*\*Expected output:\*\*)\s*(.+?)(?=\n\n|\n###|\z)`)
)
// Load reads a recipe from disk and fully parses it.
func Load(path string, params map[string]any) (*Recipe, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// split frontmatter
parts := bytes.SplitN(b, []byte("---"), 3)
if len(parts) < 3 {
return nil, fmt.Errorf("missing YAML frontmatter")
}
var r Recipe
if err := yaml.Unmarshal(parts[1], &r); err != nil {
return nil, fmt.Errorf("yaml parse: %w", err)
}
// simple template render on the whole body (so {{.package_path}} works everywhere)
tpl, err := template.New("recipe").Parse(string(parts[2]))
if err != nil {
return nil, err
}
var rendered bytes.Buffer
if err := tpl.Execute(&rendered, params); err != nil {
return nil, err
}
body := rendered.String()
// extract steps
matches := stepRe.FindAllStringSubmatch(body, -1)
for i, m := range matches {
stepNum := i + 1
title := m[2]
// crude but effective sub-section extraction
start := strings.Index(body, m[0])
end := len(body)
if i+1 < len(matches) {
end = strings.Index(body[start:], matches[i+1][0]) + start
}
section := body[start:end]
step := Step{Number: stepNum, Title: title}
for _, sub := range subRe.FindAllStringSubmatch(section, -1) {
switch sub[1] {
case "**Objective:**":
step.Objective = strings.TrimSpace(sub[2])
case "**Instructions:**":
step.Instructions = strings.TrimSpace(sub[2])
case "**Expected output:**":
step.Expected = strings.TrimSpace(sub[2])
}
}
r.Steps = append(r.Steps, step)
}
// final summary is everything after the last step
lastStepEnd := strings.LastIndex(body, matches[len(matches)-1][0])
r.FinalSummaryPrompt = strings.TrimSpace(body[lastStepEnd+len(matches[len(matches)-1][0]):])
return &r, nil
}

22
internal/recipe/runner.go Normal file
View File

@ -0,0 +1,22 @@
package recipe
import "fmt"
type Runner struct {
Recipe *Recipe
}
func NewRunner(r *Recipe) *Runner {
return &Runner{Recipe: r}
}
func (r *Runner) Run() error {
fmt.Printf("🍳 Starting recipe: %s v%s\n", r.Recipe.Name, r.Recipe.Version)
for _, step := range r.Recipe.Steps {
fmt.Printf("Step %d/%d: %s\n", step.Number, len(r.Recipe.Steps), step.Title)
// TODO: here we will send step.Instructions to the LLM
// and handle the response according to Expected
}
fmt.Println("✅ Recipe complete.")
return nil
}

26
internal/recipe/types.go Normal file
View File

@ -0,0 +1,26 @@
package recipe
type Recipe struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Version string `yaml:"version"`
Parameters map[string]Parameter `yaml:"parameters"`
AllowedShellCommands []string `yaml:"allowed_shell_commands"`
Overview string `yaml:"-"` // extracted from markdown
Steps []Step `yaml:"-"`
FinalSummaryPrompt string `yaml:"-"`
}
type Parameter struct {
Type string `yaml:"type"`
Default any `yaml:"default"`
Description string `yaml:"description"`
}
type Step struct {
Number int
Title string
Objective string
Instructions string
Expected string
}