Commit e11857fc8e4c

Vincent Demeester <vincent@sbr.pm>
2026-01-27 14:00:31
feat(review-tool): add YAML/LLM output format
Add YAML output format optimized for LLM consumption based on research showing YAML provides better accuracy (51-62%) compared to JSON (43-53%) and XML (33-56%) for nested data structures. - New output format: yaml (also aliased as 'llm') - Structured output with activities grouped by source and type - Includes relevant metadata (section, source file, duration, repo) - Fix .gitignore to not exclude cmd/review-tool directory Usage: review-tool -f yaml "past 7 days" review-tool -f llm -s org "this week" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 775ec56
Changed files (3)
tools
review-tool
cmd
review-tool
internal
output
tools/review-tool/cmd/review-tool/main.go
@@ -87,7 +87,7 @@ Arguments:
 
 Flags:
   -c, --config string   Config file (default: ~/.config/review-tool/config.yaml)
-  -f, --format string   Output format: markdown, json (default from config)
+  -f, --format string   Output format: markdown, json, yaml/llm (default from config)
   -s, --source string   Sources to include (can be repeated)
   -h, --help            Show this help message
 
@@ -148,6 +148,8 @@ func runReview(timeRangeStr string) error {
 	switch format {
 	case "json":
 		return output.WriteJSON(os.Stdout, report)
+	case "yaml", "llm":
+		return output.WriteYAML(os.Stdout, report)
 	default:
 		return output.WriteMarkdown(os.Stdout, report)
 	}
tools/review-tool/internal/output/yaml.go
@@ -0,0 +1,123 @@
+package output
+
+import (
+	"fmt"
+	"io"
+	"sort"
+	"strings"
+
+	"github.com/vdemeester/home/tools/review-tool/internal/activity"
+)
+
+// WriteYAML writes the report in YAML format optimized for LLM consumption.
+// YAML format provides better accuracy for LLM understanding compared to JSON or XML.
+func WriteYAML(w io.Writer, report *activity.ReviewReport) error {
+	fmt.Fprintf(w, "# Activity Review: %s\n", report.TimeRange.Description)
+	fmt.Fprintf(w, "period:\n")
+	fmt.Fprintf(w, "  start: %s\n", report.TimeRange.Start.Format("2006-01-02"))
+	fmt.Fprintf(w, "  end: %s\n", report.TimeRange.End.Format("2006-01-02"))
+	fmt.Fprintf(w, "generated: %s\n", report.GeneratedAt.Format("2006-01-02 15:04"))
+	fmt.Fprintln(w)
+
+	// Summary
+	if report.Summary != nil && report.Summary.TotalItems > 0 {
+		fmt.Fprintln(w, "summary:")
+		fmt.Fprintf(w, "  total_activities: %d\n", report.Summary.TotalItems)
+
+		cats := make([]string, 0, len(report.Summary.ItemsByCategory))
+		for cat := range report.Summary.ItemsByCategory {
+			cats = append(cats, cat)
+		}
+		sort.Strings(cats)
+
+		fmt.Fprintln(w, "  by_category:")
+		for _, cat := range cats {
+			fmt.Fprintf(w, "    %s: %d\n", cat, report.Summary.ItemsByCategory[cat])
+		}
+		fmt.Fprintln(w)
+	}
+
+	// Activities by source
+	fmt.Fprintln(w, "activities:")
+	sourceOrder := []string{"github", "org", "jira", "claude"}
+	for _, source := range sourceOrder {
+		act, ok := report.Activities[source]
+		if !ok || len(act.Items) == 0 {
+			continue
+		}
+
+		fmt.Fprintf(w, "  %s:\n", source)
+
+		if act.Error != "" {
+			fmt.Fprintf(w, "    error: %q\n", act.Error)
+			continue
+		}
+
+		// Group by type
+		byType := groupByType(act.Items)
+		types := make([]string, 0, len(byType))
+		for t := range byType {
+			types = append(types, t)
+		}
+		sort.Strings(types)
+
+		for _, typ := range types {
+			items := byType[typ]
+			fmt.Fprintf(w, "    %s:\n", typ)
+
+			// Sort by timestamp descending
+			sort.Slice(items, func(i, j int) bool {
+				return items[i].Timestamp.After(items[j].Timestamp)
+			})
+
+			for _, item := range items {
+				writeYAMLItem(w, item)
+			}
+		}
+	}
+
+	return nil
+}
+
+func writeYAMLItem(w io.Writer, item activity.ActivityItem) {
+	// Escape title for YAML if needed
+	title := item.Title
+	if strings.ContainsAny(title, ":\n\"'") {
+		title = fmt.Sprintf("%q", title)
+	}
+
+	fmt.Fprintf(w, "      - title: %s\n", title)
+	fmt.Fprintf(w, "        timestamp: %s\n", item.Timestamp.Format("2006-01-02 15:04"))
+
+	if item.URL != "" {
+		fmt.Fprintf(w, "        url: %s\n", item.URL)
+	}
+
+	// Include relevant metadata
+	if section := item.Metadata["section"]; section != "" {
+		fmt.Fprintf(w, "        section: %s\n", section)
+	}
+	if file := item.Metadata["file"]; file != "" {
+		// Shorten archive paths
+		if strings.Contains(file, "/archive/") {
+			parts := strings.Split(file, "/archive/")
+			if len(parts) > 1 {
+				fmt.Fprintf(w, "        source: archive/%s\n", parts[1])
+			}
+		}
+	}
+	if duration := item.Metadata["duration"]; duration != "" {
+		fmt.Fprintf(w, "        duration: %s\n", duration)
+	}
+	if repo := item.Metadata["repo"]; repo != "" {
+		fmt.Fprintf(w, "        repo: %s\n", repo)
+	}
+
+	if item.Description != "" {
+		desc := item.Description
+		if strings.ContainsAny(desc, ":\n\"'") {
+			desc = fmt.Sprintf("%q", desc)
+		}
+		fmt.Fprintf(w, "        description: %s\n", desc)
+	}
+}
tools/review-tool/.gitignore
@@ -1,4 +1,4 @@
 # Binaries
-review-tool
-review-tool-debug
+/review-tool
+/review-tool-debug
 *.exe