package prompts // Package prompts is eeco's versioned canonical prompt library: one named, // embedded text/template per AI-using workflow, the single reviewable source // of truth for the instruction strings eeco sends to a model. The bodies live // in templates/*.tmpl; the prompt name is the file name without the extension. // // At v1.0.0 the library ships two prompts: GetProjectType (Layer 4 init // detection) and ManifestSummary (per-directory .ai.json body generation). // Existing scattered prompts (evolve, understand, ...) migrate as additive // v1.x slices. import ( "bytes" "embed" "fmt" "io/fs" "sort" "strings" "text/template" ) //go:embed templates/*.tmpl var templatesFS embed.FS // Prompt names. Each MUST match a templates/.tmpl file. const ( GetProjectType = "get-project-type" ManifestSummary = "manifest-summary" ) // funcs are the template helpers available to every prompt template. var funcs = template.FuncMap{"join": strings.Join} type entry struct { raw string tmpl *template.Template } // registry is built once at package load. A malformed shipped template panics // here on purpose — a prompt template is a build-time artifact, not runtime // input, so a parse failure must surface immediately, not at first render. var registry = mustLoad() func mustLoad() map[string]entry { files, err := fs.ReadDir(templatesFS, "templates") if err != nil { panic("prompts: read templates dir: " + err.Error()) } reg := make(map[string]entry, len(files)) for _, f := range files { if f.IsDir() || !strings.HasSuffix(f.Name(), ".tmpl") { continue } body, err := templatesFS.ReadFile("templates/" + f.Name()) if err != nil { panic("prompts: read template " + f.Name() + ": " + err.Error()) } name := strings.TrimSuffix(f.Name(), ".tmpl") t, err := template.New(name).Funcs(funcs).Parse(string(body)) if err != nil { panic(fmt.Sprintf("prompts: parse %s: %v", name, err)) } reg[name] = entry{raw: string(body), tmpl: t} } return reg } // Names returns every available prompt name, sorted. func Names() []string { out := make([]string, 0, len(registry)) for n := range registry { out = append(out, n) } sort.Strings(out) return out } // Get returns the raw template body for a prompt — the canonical text an // operator audits via `eeco show prompt `. func Get(name string) (string, error) { e, ok := registry[name] if !ok { return "", fmt.Errorf("unknown prompt %q", name) } return e.raw, nil } // Render executes a prompt template against data and returns the result. func Render(name string, data any) (string, error) { e, ok := registry[name] if !ok { return "", fmt.Errorf("unknown prompt %q", name) } var b bytes.Buffer if err := e.tmpl.Execute(&b, data); err != nil { return "", fmt.Errorf("render %s: %w", name, err) } return b.String(), nil } // GetProjectTypeData is the render input for the GetProjectType prompt. type GetProjectTypeData struct { Categories []Category Tree []string Description string } // Category is one catalog entry rendered into the GetProjectType prompt. type Category struct { Category string Description string PickWhen string Dirs []string } // ManifestSummaryData is the render input for the ManifestSummary prompt. type ManifestSummaryData struct { Dir string Items []ManifestItem } // ManifestItem is one directory entry rendered into the ManifestSummary prompt. type ManifestItem struct { Path string Kind string }