refactor: split templates/ into prompts/, tasks/, flows/

Three distinct concepts in lib/:
  prompts/  — System prompts (PROMPT.md, HOW to work)
  tasks/    — Structured task plans (PLAN.md, WHAT to do)
  flows/    — Multi-phase workflows (orchestration)
  personas/ — Domain/role system prompts (WHO you are)

API updated:
  prompts.Prompt("coding")     — system prompt
  prompts.Task("bug-fix")      — task plan
  prompts.Flow("prod-push-polish") — workflow
  prompts.Template()           — backwards compat (searches both)

templates/ dir reserved for future output templates
(CodeRabbit report formatting, CLI output parsing, etc.)

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-17 22:26:46 +00:00
parent f1e1c08aff
commit 433deb1c30
18 changed files with 130 additions and 47 deletions

View file

@ -1,13 +1,20 @@
// SPDX-License-Identifier: EUPL-1.2
// Package prompts provides embedded prompt templates and personas for agent dispatch.
// Templates and personas are loaded from lib/ at compile time via go:embed.
// Package prompts provides embedded prompt content for agent dispatch.
// All content is loaded from lib/ at compile time via go:embed.
//
// Structure:
//
// lib/prompts/ — System prompts (PROMPT.md content, HOW to work)
// lib/tasks/ — Structured task plans (PLAN.md, WHAT to do)
// lib/flows/ — Multi-phase workflows (orchestration sequences)
// lib/personas/ — Domain/role system prompts (WHO you are)
//
// Usage:
//
// template, _ := prompts.Template("bug-fix")
// persona, _ := prompts.Persona("engineering/engineering-security-engineer")
// all := prompts.ListTemplates()
// prompt, _ := prompts.Prompt("coding")
// task, _ := prompts.Task("bug-fix")
// persona, _ := prompts.Persona("secops/developer")
package prompts
import (
@ -17,18 +24,42 @@ import (
"strings"
)
//go:embed lib/templates/*.yaml lib/templates/*.md
var templateFS embed.FS
//go:embed lib/prompts/*.md
var promptFS embed.FS
//go:embed lib/tasks/*.yaml
var taskFS embed.FS
//go:embed lib/flows/*.md
var flowFS embed.FS
//go:embed lib/personas
var personaFS embed.FS
// Template returns the content of a prompt template by slug.
// Slug examples: "bug-fix", "code-review", "security".
// Prompt returns a system prompt by slug (written as PROMPT.md).
// Slugs: "coding", "verify", "conventions", "security", "default".
func Prompt(slug string) (string, error) {
data, err := promptFS.ReadFile("lib/prompts/" + slug + ".md")
if err != nil {
return "", err
}
return string(data), nil
}
// Template is an alias for Prompt (backwards compatibility).
func Template(slug string) (string, error) {
// Try .yaml first, then .yml, then .md
for _, ext := range []string{".yaml", ".yml", ".md"} {
data, err := templateFS.ReadFile("lib/templates/" + slug + ext)
// Try prompts first, then tasks
if content, err := Prompt(slug); err == nil {
return content, nil
}
return Task(slug)
}
// Task returns a structured task plan by slug (written as PLAN.md).
// Slugs: "bug-fix", "new-feature", "refactor", "code-review", etc.
func Task(slug string) (string, error) {
for _, ext := range []string{".yaml", ".yml"} {
data, err := taskFS.ReadFile("lib/tasks/" + slug + ext)
if err == nil {
return string(data), nil
}
@ -36,9 +67,17 @@ func Template(slug string) (string, error) {
return "", fs.ErrNotExist
}
// Persona returns the content of a persona by path.
// Path examples: "engineering/engineering-security-engineer",
// "testing/testing-api-tester", "specialized/blockchain-security-auditor".
// Flow returns a multi-phase workflow by slug.
func Flow(slug string) (string, error) {
data, err := flowFS.ReadFile("lib/flows/" + slug + ".md")
if err != nil {
return "", err
}
return string(data), nil
}
// Persona returns a domain/role system prompt by path.
// Paths: "secops/developer", "code/backend-architect", "smm/tiktok-strategist".
func Persona(path string) (string, error) {
data, err := personaFS.ReadFile("lib/personas/" + path + ".md")
if err != nil {
@ -47,23 +86,24 @@ func Persona(path string) (string, error) {
return string(data), nil
}
// ListTemplates returns all available template slugs.
// ListPrompts returns all available prompt slugs.
func ListPrompts() []string {
return listDir(promptFS, "lib/prompts")
}
// ListTasks returns all available task plan slugs.
func ListTasks() []string {
return listDir(taskFS, "lib/tasks")
}
// ListFlows returns all available flow slugs.
func ListFlows() []string {
return listDir(flowFS, "lib/flows")
}
// ListTemplates returns all prompt + task slugs (backwards compatibility).
func ListTemplates() []string {
entries, err := templateFS.ReadDir("lib/templates")
if err != nil {
return nil
}
var slugs []string
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
ext := filepath.Ext(name)
slug := strings.TrimSuffix(name, ext)
slugs = append(slugs, slug)
}
return slugs
return append(ListPrompts(), ListTasks()...)
}
// ListPersonas returns all available persona paths.
@ -74,7 +114,6 @@ func ListPersonas() []string {
return nil
}
if strings.HasSuffix(path, ".md") {
// Strip prefix and extension: lib/personas/engineering/foo.md → engineering/foo
rel := strings.TrimPrefix(path, "lib/personas/")
rel = strings.TrimSuffix(rel, ".md")
paths = append(paths, rel)
@ -84,12 +123,20 @@ func ListPersonas() []string {
return paths
}
// TemplateFS returns the raw embedded filesystem for templates.
func TemplateFS() embed.FS {
return templateFS
}
// PersonaFS returns the raw embedded filesystem for personas.
func PersonaFS() embed.FS {
return personaFS
// listDir returns slugs (filename without extension) from an embedded directory.
func listDir(fsys embed.FS, dir string) []string {
entries, err := fsys.ReadDir(dir)
if err != nil {
return nil
}
var slugs []string
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
ext := filepath.Ext(name)
slugs = append(slugs, strings.TrimSuffix(name, ext))
}
return slugs
}

View file

@ -10,21 +10,57 @@ import (
"github.com/stretchr/testify/require"
)
func TestTemplate_Good_YAML(t *testing.T) {
content, err := Template("bug-fix")
func TestPrompt_Good(t *testing.T) {
content, err := Prompt("coding")
require.NoError(t, err)
assert.Contains(t, content, "SANDBOX")
assert.Contains(t, content, "Closeout Sequence")
}
func TestPrompt_Bad_NotFound(t *testing.T) {
_, err := Prompt("nonexistent")
assert.Error(t, err)
}
func TestTask_Good(t *testing.T) {
content, err := Task("bug-fix")
require.NoError(t, err)
assert.Contains(t, content, "name:")
}
func TestTemplate_Good_MD(t *testing.T) {
content, err := Template("prod-push-polish")
func TestTask_Bad_NotFound(t *testing.T) {
_, err := Task("nonexistent")
assert.Error(t, err)
}
func TestTemplate_Good_BackwardsCompat(t *testing.T) {
// Template() should find prompts
content, err := Template("coding")
require.NoError(t, err)
assert.Contains(t, content, "SANDBOX")
// Template() should also find tasks
content, err = Template("bug-fix")
require.NoError(t, err)
assert.Contains(t, content, "name:")
}
func TestFlow_Good(t *testing.T) {
content, err := Flow("prod-push-polish")
require.NoError(t, err)
assert.True(t, len(content) > 0)
}
func TestTemplate_Bad_NotFound(t *testing.T) {
_, err := Template("nonexistent-template")
assert.Error(t, err)
func TestListPrompts_Good(t *testing.T) {
list := ListPrompts()
assert.Contains(t, list, "coding")
assert.Contains(t, list, "verify")
}
func TestListTasks_Good(t *testing.T) {
list := ListTasks()
assert.Contains(t, list, "bug-fix")
assert.Contains(t, list, "refactor")
}
func TestPersona_Good(t *testing.T) {