Skip to content
Open
Show file tree
Hide file tree
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
39 changes: 2 additions & 37 deletions internal/executor/workflow_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,43 +195,8 @@ func (we *WorkflowExecutor) loadAndFormatInitiativeNotes(initiativeID string) st
return ""
}

// Group notes by type (patterns, warnings, learnings, handoffs)
byType := make(map[string][]db.InitiativeNote)
for _, n := range filtered {
byType[n.NoteType] = append(byType[n.NoteType], n)
}

// Format notes grouped by type in a consistent order
var sb strings.Builder
typeOrder := []struct {
noteType string
label string
emoji string
}{
{db.NoteTypePattern, "Patterns", "📋"},
{db.NoteTypeWarning, "Warnings", "⚠️"},
{db.NoteTypeLearning, "Learnings", "💡"},
{db.NoteTypeHandoff, "Handoffs", "🤝"},
}

for _, t := range typeOrder {
notes := byType[t.noteType]
if len(notes) == 0 {
continue
}

fmt.Fprintf(&sb, "**%s %s:**\n", t.emoji, t.label)
for _, n := range notes {
fmt.Fprintf(&sb, "- %s", n.Content)
if n.SourceTask != "" {
fmt.Fprintf(&sb, " *(from %s)*", n.SourceTask)
}
sb.WriteString("\n")
}
sb.WriteString("\n")
}

return strings.TrimSuffix(sb.String(), "\n")
// Delegate formatting to the helper function
return variable.FormatInitiativeNotes(filtered)
}

// loadProjectDetectionContext loads project detection data into the resolution context.
Expand Down
60 changes: 60 additions & 0 deletions internal/variable/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"regexp"
"strings"
"time"

"github.com/randalmurphal/orc/internal/db"
)

// Resolver resolves variable definitions to their values.
Expand Down Expand Up @@ -447,6 +449,64 @@ func formatInitiativeContext(rctx *ResolutionContext) string {
return sb.String()
}

// FormatInitiativeNotes formats initiative notes as markdown grouped by type.
//
// Notes are filtered by visibility rules before being passed to this function:
// - Human notes: always visible
// - Agent notes: only visible if graduated (met strict quality bar)
//
// The output groups notes in a fixed order for consistent display:
// 1. Patterns (📋) - code conventions, architectural rules
// 2. Warnings (⚠️) - gotchas, things to avoid
// 3. Learnings (💡) - insights gained from implementation
// 4. Handoffs (🤝) - context for the next task
//
// Each note includes its source task reference for traceability.
// Returns empty string if notes slice is empty.
func FormatInitiativeNotes(notes []db.InitiativeNote) string {
if len(notes) == 0 {
return ""
}

// Group notes by type
byType := make(map[string][]db.InitiativeNote)
for _, n := range notes {
byType[n.NoteType] = append(byType[n.NoteType], n)
}

// Format notes grouped by type in a consistent order
var sb strings.Builder
typeOrder := []struct {
noteType string
label string
emoji string
}{
{db.NoteTypePattern, "Patterns", "📋"},
{db.NoteTypeWarning, "Warnings", "⚠️"},
{db.NoteTypeLearning, "Learnings", "💡"},
{db.NoteTypeHandoff, "Handoffs", "🤝"},
}

for _, t := range typeOrder {
notes := byType[t.noteType]
if len(notes) == 0 {
continue
}

fmt.Fprintf(&sb, "**%s %s:**\n", t.emoji, t.label)
for _, n := range notes {
fmt.Fprintf(&sb, "- %s", n.Content)
if n.SourceTask != "" {
fmt.Fprintf(&sb, " *(from %s)*", n.SourceTask)
}
sb.WriteString("\n")
}
sb.WriteString("\n")
}

return strings.TrimSuffix(sb.String(), "\n")
}

// RenderTemplate applies variable substitution to a template string.
// Variables use the {{VAR}} format. Missing variables are replaced with empty strings.
// Also handles {{#if VAR}}...{{/if}} conditional blocks.
Expand Down