diff --git a/internal/recipe/loader.go b/internal/recipe/loader.go index c8fb992..2421c7c 100644 --- a/internal/recipe/loader.go +++ b/internal/recipe/loader.go @@ -12,15 +12,10 @@ import ( ) var ( - // stepRe finds every "### Step N: Title" heading stepRe = regexp.MustCompile(`(?m)^### Step (\d+): (.+)$`) - - // subRe finds the three labelled sections inside each step (no Perl lookahead) - subRe = regexp.MustCompile(`(?m)^(\*\*(?:Objective|Instructions|Expected output):\*\*)\s*(.+?)(?:\n\n|\n###|\z)`) + subRe = regexp.MustCompile(`(?m)^(\*\*(?:Objective|Instructions|Expected output):\*\*)\s*(.+?)(?:\n\n|\n###|\z)`) ) -// Load reads a recipe from disk and fully parses it. -// Load reads a recipe from disk and fully parses it. func Load(path string, userParams map[string]any) (*Recipe, error) { b, err := os.ReadFile(path) if err != nil { @@ -37,7 +32,7 @@ func Load(path string, userParams map[string]any) (*Recipe, error) { return nil, fmt.Errorf("yaml parse: %w", err) } - // Apply defaults from YAML if user didn't supply them + // Apply defaults if r.Parameters == nil { r.Parameters = make(map[string]Parameter) } @@ -50,7 +45,7 @@ func Load(path string, userParams map[string]any) (*Recipe, error) { } } - // render templates with defaults applied + // Render templates tpl, err := template.New("recipe").Parse(string(parts[2])) if err != nil { return nil, err @@ -59,25 +54,26 @@ func Load(path string, userParams map[string]any) (*Recipe, error) { if err := tpl.Execute(&rendered, params); err != nil { return nil, err } - - if err := tpl.Execute(&rendered, params); err != nil { - return nil, err - } - body := rendered.String() - // extract steps + // Extract Overview + if idx := strings.Index(body, "## Execution Steps"); idx != -1 { + r.Overview = strings.TrimSpace(body[:idx]) + } + + // Extract steps — split-based to guarantee no duplicates 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 + nextStart := strings.Index(body[start:], matches[i+1][0]) + end = start + nextStart } + section := body[start:end] step := Step{Number: stepNum, Title: title} @@ -94,9 +90,12 @@ func Load(path string, userParams map[string]any) (*Recipe, error) { 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]):]) + // Final summary = everything after the last step + if len(matches) > 0 { + lastMatch := matches[len(matches)-1][0] + lastIdx := strings.LastIndex(body, lastMatch) + r.FinalSummaryPrompt = strings.TrimSpace(body[lastIdx+len(lastMatch):]) + } return &r, nil } diff --git a/internal/recipe/runner.go b/internal/recipe/runner.go index ad663fd..5673fde 100644 --- a/internal/recipe/runner.go +++ b/internal/recipe/runner.go @@ -25,10 +25,7 @@ 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) - // Build a clear, self-contained prompt for this step - prompt := fmt.Sprintf(`You are an expert sous-chef executing a recipe with precision. - -Recipe Overview: + prompt := fmt.Sprintf(`Recipe Overview: %s Previous step results (for context): @@ -39,35 +36,34 @@ Objective: %s Instructions: %s Expected output format: %s -Execute the step now. Be concise and follow the expected output format exactly.`, - r.Recipe.Overview, // we'll extract this too if you want, or leave as-is - strings.Join(previousResults, "\n\n"), +Execute this step now. Respond ONLY with the expected output format — no explanations, no extra text.`, + r.Recipe.Overview, + strings.Join(previousResults, "\n\n---\n\n"), step.Objective, step.Instructions, step.Expected) - // Use the same streaming client as every other command messages := []map[string]string{ - {"role": "system", "content": "You are a precise, no-nonsense coding sous-chef."}, + {"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": "user", "content": prompt}, } response := r.Client.Stream(messages, r.Model) - fmt.Println() // extra newline after streaming finishes + fmt.Println() previousResults = append(previousResults, fmt.Sprintf("Step %d result:\n%s", step.Number, response)) } - // Final summary step + // 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"), r.Recipe.FinalSummaryPrompt) +%s`, strings.Join(previousResults, "\n\n---\n\n"), r.Recipe.FinalSummaryPrompt) messages := []map[string]string{ - {"role": "system", "content": "You are a precise, no-nonsense coding sous-chef."}, + {"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)