// Package playbooks is eeco's shipped, neutral cockpit-playbook library: // one embedded JSON source per AI procedure, the single reviewable source // of truth for the playbooks eeco emits as harness config. It mirrors // internal/prompts (embedded sources + a registry), but its unit is a // cockpit.Playbook rather than a text template. // // Dependency direction (no cycle): this package imports internal/cockpit // for the Playbook type; cockpit never imports playbooks. cmd/eeco wires // the two — playbooks.Get(name) feeds cockpit.Generate. // // At C1 the library ships one source: handover. The general and // parameterized playbooks (commit, doc-drift, …) migrate as additive C2 // slices. package playbooks import ( "embed" "encoding/json" "fmt" "io/fs" "sort" "strings" "github.com/ajhahnde/eeco/internal/cockpit" ) //go:embed data/*.json var dataFS embed.FS // entry pairs a parsed Playbook with the raw JSON an operator audits via // `eeco cockpit show`. type entry struct { pb cockpit.Playbook raw string } // registry is built once at package load. A malformed shipped source // panics here on purpose — a playbook source is a build-time artifact, not // runtime input, so a parse failure must surface immediately (the // internal/prompts precedent). var registry = mustLoad() func mustLoad() map[string]entry { files, err := fs.ReadDir(dataFS, "data") if err != nil { panic("playbooks: read data dir: " + err.Error()) } reg := make(map[string]entry, len(files)) for _, f := range files { if f.IsDir() || !strings.HasSuffix(f.Name(), ".json") { continue } body, err := dataFS.ReadFile("data/" + f.Name()) if err != nil { panic("playbooks: read source " + f.Name() + ": " + err.Error()) } var pb cockpit.Playbook if err := json.Unmarshal(body, &pb); err != nil { panic(fmt.Sprintf("playbooks: parse %s: %v", f.Name(), err)) } name := strings.TrimSuffix(f.Name(), ".json") if pb.Name != name { panic(fmt.Sprintf("playbooks: %s declares name %q (must match file)", f.Name(), pb.Name)) } reg[name] = entry{pb: pb, raw: string(body)} } return reg } // Names returns every available playbook name, sorted. func Names() []string { out := make([]string, 0, len(registry)) for n := range registry { out = append(out, n) } sort.Strings(out) return out } // All returns every registered Playbook, ordered by Name (mirroring Names), // so the aggregate renderers receive a deterministic set. func All() []cockpit.Playbook { out := make([]cockpit.Playbook, 0, len(registry)) for _, n := range Names() { out = append(out, registry[n].pb) } return out } // Get returns the parsed Playbook for name. func Get(name string) (cockpit.Playbook, error) { e, ok := registry[name] if !ok { return cockpit.Playbook{}, fmt.Errorf("unknown playbook %q (known: %s)", name, strings.Join(Names(), ", ")) } return e.pb, nil } // Raw returns the canonical JSON source for name — the text an operator // audits via `eeco cockpit show`. func Raw(name string) (string, error) { e, ok := registry[name] if !ok { return "", fmt.Errorf("unknown playbook %q (known: %s)", name, strings.Join(Names(), ", ")) } return e.raw, nil }