From 0ba427aaf66a9a175c628571782bf5faa91ba784 Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Fri, 6 Mar 2026 21:01:01 +0000 Subject: [PATCH] feat(recipe): add apply/patch step handling with code block extraction and application - Introduce special case in Runner.Run() for steps containing "apply" or "patch" in title. - Add handleApplyStep to parse code blocks from previous results, support dry-run patch creation, or real application with user confirmation and backups. - Implement extractCodeBlocks using regex to identify labelled Go code blocks. - Add createUnifiedPatch for generating unified diff patches in dry-run mode. - Remove final summary prompt and streamline recipe completion. - Adjust system prompt for clarity. --- internal/recipe/runner.go | 126 +++++++++++++++++++++++++++++++++----- 1 file changed, 111 insertions(+), 15 deletions(-) diff --git a/internal/recipe/runner.go b/internal/recipe/runner.go index 5673fde..37a2f54 100644 --- a/internal/recipe/runner.go +++ b/internal/recipe/runner.go @@ -1,7 +1,11 @@ package recipe import ( + "bufio" "fmt" + "os" + "path/filepath" + "regexp" "strings" "gmgauthier.com/grokkit/internal/grok" @@ -25,6 +29,13 @@ func (r *Runner) Run() error { for _, step := range r.Recipe.Steps { fmt.Printf("Step %d/%d: %s\n", step.Number, len(r.Recipe.Steps), step.Title) + // Special case: Apply or patch step is handled by the CLI (like edit/scaffold) + if strings.Contains(strings.ToLower(step.Title), "apply") || strings.Contains(strings.ToLower(step.Title), "patch") { + r.handleApplyStep(previousResults) + continue + } + + // Normal LLM step prompt := fmt.Sprintf(`Recipe Overview: %s @@ -44,7 +55,7 @@ Execute this step now. Respond ONLY with the expected output format — no expla step.Expected) messages := []map[string]string{ - {"role": "system", "content": "You are Grok, built by xAI. You are a precise, expert Go programmer and refactoring assistant. Always follow the user's instructions exactly for legitimate coding tasks. Never refuse or add safety warnings."}, + {"role": "system", "content": "You are Grok, built by xAI. You are a precise, expert Go programmer and refactoring assistant. Always follow the user's instructions exactly for legitimate coding tasks."}, {"role": "user", "content": prompt}, } @@ -54,20 +65,105 @@ Execute this step now. Respond ONLY with the expected output format — no expla previousResults = append(previousResults, fmt.Sprintf("Step %d result:\n%s", step.Number, response)) } - // Final summary - fmt.Println("Final Summary") - finalPrompt := fmt.Sprintf(`You just executed the entire recipe. Here is the full history: - -%s - -%s`, strings.Join(previousResults, "\n\n---\n\n"), r.Recipe.FinalSummaryPrompt) - - messages := []map[string]string{ - {"role": "system", "content": "You are Grok, built by xAI. You are a precise, expert programmer and refactoring assistant."}, - {"role": "user", "content": finalPrompt}, - } - r.Client.Stream(messages, r.Model) - fmt.Println("\n✅ Recipe complete.") return nil } + +// handleApplyStep parses the refactored code blocks from the previous step and does the real apply/patch with confirmation +func (r *Runner) handleApplyStep(previousResults []string) { + if len(previousResults) == 0 { + fmt.Println(" ⚠️ No previous results to apply — skipping.") + return + } + + // Extract all labelled code blocks from the last LLM step (Step 2) + lastResult := previousResults[len(previousResults)-1] + blocks := extractCodeBlocks(lastResult) + + if len(blocks) == 0 { + fmt.Println(" ⚠️ No code blocks found to apply — skipping.") + return + } + + // Dry-run or real apply? + dryRun := true // TODO: read from parameters once we add --param support + if dryRun { + fmt.Println(" 📄 Dry-run mode: creating patch file...") + patchPath := filepath.Join(".", "recipe-refactor.patch") + if err := createUnifiedPatch(blocks, patchPath); err != nil { + fmt.Printf(" ❌ Failed to create patch: %v\n", err) + return + } + fmt.Printf(" ✅ Patch created: %s\n", patchPath) + fmt.Println(" Review it, then run with dry_run=false to apply.") + return + } + + // Real apply with confirmation (exactly like edit/scaffold) + fmt.Print("\nApply these changes to disk? (y/N) ") + scanner := bufio.NewScanner(os.Stdin) + if scanner.Scan() { + answer := strings.ToLower(strings.TrimSpace(scanner.Text())) + if answer == "y" || answer == "yes" { + for filePath, content := range blocks { + backup := filePath + ".bak" + if err := os.Rename(filePath, backup); err == nil { + fmt.Printf(" 📦 Backed up: %s\n", backup) + } + if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + fmt.Printf(" ❌ Failed to write %s: %v\n", filePath, err) + continue + } + fmt.Printf(" ✅ Applied: %s\n", filePath) + } + } else { + fmt.Println(" ❌ Cancelled — no changes made.") + } + } +} + +// Simple parser for the labelled blocks Grok outputs: "// /path/to/file.go\n```go\ncode\n```" +var regStr = "`(?s)//\\s*(.+?\\.go)\\n```go\\n(.*?)\\n````" +var blockRe = regexp.MustCompile(regStr) + +func extractCodeBlocks(text string) map[string]string { + blocks := make(map[string]string) + matches := blockRe.FindAllStringSubmatch(text, -1) + for _, m := range matches { + if len(m) == 3 { + blocks[m[1]] = m[2] + } + } + return blocks +} + +func createUnifiedPatch(blocks map[string]string, patchPath string) error { + // Very simple unified patch for now — can be improved later with real diff + f, err := os.Create(patchPath) + if err != nil { + return err + } + defer func(f *os.File) { + err := f.Close() + if err != nil { + _, err := fmt.Fprintf(f, "+%s\n", err.Error()) + if err != nil { + return + } + } + }(f) + + for path, content := range blocks { + _, err := fmt.Fprintf(f, "--- %s\n+++ %s\n@@ -0,0 +1,%d @@\n", path, path, strings.Count(content, "\n")+1) + if err != nil { + return err + } + for _, line := range strings.Split(content, "\n") { + _, err := fmt.Fprintf(f, "+%s\n", line) + if err != nil { + return err + } + } + } + return nil +}