agent/pkg/lib/lib.go
Snider 3c5e6d6498 fix(lib): ExtractWorkspace now recurses into subdirectories
Was skipping directories entirely (`if entry.IsDir() { continue }`),
so .core/reference/ and its contents were never extracted.
Replaced fs.ReadDir loop with fs.WalkDir to handle nested dirs.

Added tests: CreatesFiles, CreatesSubdirectories, TemplateSubstitution.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 08:10:56 +00:00

240 lines
5.5 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
// Package lib provides embedded content for agent dispatch.
// Prompts, tasks, flows, personas, and workspace templates.
//
// Structure:
//
// prompt/ — System prompts (HOW to work)
// task/ — Structured task plans (WHAT to do)
// task/code/ — Code-specific tasks (review, refactor, etc.)
// flow/ — Build/release workflows per language/tool
// persona/ — Domain/role system prompts (WHO you are)
// workspace/ — Agent workspace templates (WHERE to work)
//
// Usage:
//
// prompt, _ := lib.Prompt("coding")
// task, _ := lib.Task("code/review")
// persona, _ := lib.Persona("secops/developer")
// flow, _ := lib.Flow("go")
// lib.ExtractWorkspace("default", "/tmp/ws", data)
package lib
import (
"bytes"
"embed"
"io/fs"
"os"
"path/filepath"
"text/template"
core "dappco.re/go/core"
)
//go:embed prompt/*.md
var promptFS embed.FS
//go:embed all:task
var taskFS embed.FS
//go:embed flow/*.md
var flowFS embed.FS
//go:embed persona
var personaFS embed.FS
//go:embed all:workspace
var workspaceFS embed.FS
// --- Prompts ---
// Template tries Prompt then Task (backwards compat).
func Template(slug string) (string, error) {
if content, err := Prompt(slug); err == nil {
return content, nil
}
return Task(slug)
}
func Prompt(slug string) (string, error) {
data, err := promptFS.ReadFile("prompt/" + slug + ".md")
if err != nil {
return "", err
}
return string(data), nil
}
func Task(slug string) (string, error) {
for _, ext := range []string{".md", ".yaml", ".yml"} {
data, err := taskFS.ReadFile("task/" + slug + ext)
if err == nil {
return string(data), nil
}
}
return "", fs.ErrNotExist
}
func TaskBundle(slug string) (string, map[string]string, error) {
main, err := Task(slug)
if err != nil {
return "", nil, err
}
bundleDir := "task/" + slug
entries, err := fs.ReadDir(taskFS, bundleDir)
if err != nil {
return main, nil, nil
}
bundle := make(map[string]string)
for _, e := range entries {
if e.IsDir() {
continue
}
data, err := taskFS.ReadFile(bundleDir + "/" + e.Name())
if err == nil {
bundle[e.Name()] = string(data)
}
}
return main, bundle, nil
}
func Flow(slug string) (string, error) {
data, err := flowFS.ReadFile("flow/" + slug + ".md")
if err != nil {
return "", err
}
return string(data), nil
}
func Persona(path string) (string, error) {
data, err := personaFS.ReadFile("persona/" + path + ".md")
if err != nil {
return "", err
}
return string(data), nil
}
// --- Workspace Templates ---
// WorkspaceData is the data passed to workspace templates.
type WorkspaceData struct {
Repo string
Branch string
Task string
Agent string
Language string
Prompt string
Persona string
Flow string
Context string
Recent string
Dependencies string
Conventions string
RepoDescription string
BuildCmd string
TestCmd string
}
// ExtractWorkspace creates an agent workspace from a template.
// Template names: "default", "security", "review".
func ExtractWorkspace(tmplName, targetDir string, data *WorkspaceData) error {
wsDir := "workspace/" + tmplName
if err := os.MkdirAll(targetDir, 0755); err != nil {
return err
}
return fs.WalkDir(workspaceFS, wsDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Get relative path from template root
rel, err := filepath.Rel(wsDir, path)
if err != nil || rel == "." {
return nil
}
targetPath := filepath.Join(targetDir, rel)
if d.IsDir() {
return os.MkdirAll(targetPath, 0755)
}
content, err := fs.ReadFile(workspaceFS, path)
if err != nil {
return err
}
// Process .tmpl files through text/template
outputName := filepath.Base(targetPath)
if core.HasSuffix(outputName, ".tmpl") {
outputName = core.TrimSuffix(outputName, ".tmpl")
targetPath = filepath.Join(filepath.Dir(targetPath), outputName)
tmpl, err := template.New(outputName).Parse(string(content))
if err != nil {
return err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return err
}
content = buf.Bytes()
}
return os.WriteFile(targetPath, content, 0644)
})
}
// --- List Functions ---
func ListPrompts() []string { return listDir(promptFS, "prompt") }
func ListFlows() []string { return listDir(flowFS, "flow") }
func ListWorkspaces() []string { return listDir(workspaceFS, "workspace") }
func ListTasks() []string {
var slugs []string
fs.WalkDir(taskFS, "task", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
rel := core.TrimPrefix(path, "task/")
ext := filepath.Ext(rel)
slugs = append(slugs, core.TrimSuffix(rel, ext))
return nil
})
return slugs
}
func ListPersonas() []string {
var paths []string
fs.WalkDir(personaFS, "persona", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
if core.HasSuffix(path, ".md") {
rel := core.TrimPrefix(path, "persona/")
rel = core.TrimSuffix(rel, ".md")
paths = append(paths, rel)
}
return nil
})
return paths
}
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 {
name := e.Name()
if e.IsDir() {
slugs = append(slugs, name)
continue
}
slugs = append(slugs, core.TrimSuffix(name, filepath.Ext(name)))
}
return slugs
}