feat(templates): recover local override support for role templates
Cherry-picked from lost commit 123d0b2b. Adds ability to override
embedded role templates at town or rig level:
- Town: <townRoot>/templates/roles/<role>.md.tmpl
- Rig: <rigPath>/templates/roles/<role>.md.tmpl
New APIs:
- NewWithOverrides(townRoot, rigPath string)
- HasRoleOverride(role string) bool
- RoleOverrideCount() int
Recovered by: gt-vjhf
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,22 +20,25 @@ var commandsFS embed.FS
|
||||
type Templates struct {
|
||||
roleTemplates *template.Template
|
||||
messageTemplates *template.Template
|
||||
// roleOverrides maps role name to override template content.
|
||||
// When set, these are used instead of embedded templates.
|
||||
roleOverrides map[string]*template.Template
|
||||
}
|
||||
|
||||
// RoleData contains information for rendering role contexts.
|
||||
type RoleData struct {
|
||||
Role string // mayor, witness, refinery, polecat, crew, deacon
|
||||
RigName string // e.g., "greenplace"
|
||||
TownRoot string // e.g., "/Users/steve/ai"
|
||||
TownName string // e.g., "ai" - the town identifier for session names
|
||||
WorkDir string // current working directory
|
||||
DefaultBranch string // default branch for merges (e.g., "main", "develop")
|
||||
Polecat string // polecat name (for polecat role)
|
||||
Polecats []string // list of polecats (for witness role)
|
||||
BeadsDir string // BEADS_DIR path
|
||||
IssuePrefix string // beads issue prefix
|
||||
MayorSession string // e.g., "gt-ai-mayor" - dynamic mayor session name
|
||||
DeaconSession string // e.g., "gt-ai-deacon" - dynamic deacon session name
|
||||
Role string // mayor, witness, refinery, polecat, crew, deacon
|
||||
RigName string // e.g., "greenplace"
|
||||
TownRoot string // e.g., "/Users/steve/ai"
|
||||
TownName string // e.g., "ai" - the town identifier for session names
|
||||
WorkDir string // current working directory
|
||||
DefaultBranch string // default branch for merges (e.g., "main", "develop")
|
||||
Polecat string // polecat name (for polecat role)
|
||||
Polecats []string // list of polecats (for witness role)
|
||||
BeadsDir string // BEADS_DIR path
|
||||
IssuePrefix string // beads issue prefix
|
||||
MayorSession string // e.g., "gt-ai-mayor" - dynamic mayor session name
|
||||
DeaconSession string // e.g., "gt-ai-deacon" - dynamic deacon session name
|
||||
}
|
||||
|
||||
// SpawnData contains information for spawn assignment messages.
|
||||
@@ -81,11 +84,24 @@ type HandoffData struct {
|
||||
GitDirty bool
|
||||
}
|
||||
|
||||
// New creates a new Templates instance.
|
||||
// New creates a new Templates instance with only embedded templates.
|
||||
func New() (*Templates, error) {
|
||||
t := &Templates{}
|
||||
return NewWithOverrides("", "")
|
||||
}
|
||||
|
||||
// Parse role templates
|
||||
// NewWithOverrides creates a Templates instance that checks for local overrides.
|
||||
// Override resolution order (later overrides earlier):
|
||||
// 1. Built-in templates (embedded in binary)
|
||||
// 2. Town-level overrides (<townRoot>/templates/roles/<role>.md.tmpl)
|
||||
// 3. Rig-level overrides (<rigPath>/templates/roles/<role>.md.tmpl)
|
||||
//
|
||||
// If townRoot or rigPath is empty, that level is skipped.
|
||||
func NewWithOverrides(townRoot, rigPath string) (*Templates, error) {
|
||||
t := &Templates{
|
||||
roleOverrides: make(map[string]*template.Template),
|
||||
}
|
||||
|
||||
// Parse embedded role templates
|
||||
roleTempl, err := template.ParseFS(templateFS, "roles/*.md.tmpl")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing role templates: %w", err)
|
||||
@@ -99,14 +115,51 @@ func New() (*Templates, error) {
|
||||
}
|
||||
t.messageTemplates = msgTempl
|
||||
|
||||
// Load role overrides from filesystem
|
||||
// Check both town and rig levels, rig takes precedence
|
||||
roles := []string{"mayor", "witness", "refinery", "polecat", "crew", "deacon"}
|
||||
overridePaths := []string{}
|
||||
if townRoot != "" {
|
||||
overridePaths = append(overridePaths, filepath.Join(townRoot, "templates", "roles"))
|
||||
}
|
||||
if rigPath != "" {
|
||||
overridePaths = append(overridePaths, filepath.Join(rigPath, "templates", "roles"))
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
templateName := role + ".md.tmpl"
|
||||
// Check each override path in order (later paths override earlier)
|
||||
for _, overrideDir := range overridePaths {
|
||||
overridePath := filepath.Join(overrideDir, templateName)
|
||||
if content, err := os.ReadFile(overridePath); err == nil {
|
||||
tmpl, err := template.New(templateName).Parse(string(content))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing override template %s: %w", overridePath, err)
|
||||
}
|
||||
t.roleOverrides[role] = tmpl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// RenderRole renders a role context template.
|
||||
// If an override template exists for the role, it is used instead of the embedded template.
|
||||
func (t *Templates) RenderRole(role string, data RoleData) (string, error) {
|
||||
templateName := role + ".md.tmpl"
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Check for override template first
|
||||
if override, ok := t.roleOverrides[role]; ok {
|
||||
if err := override.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("rendering override template %s: %w", templateName, err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Fall back to embedded template
|
||||
if err := t.roleTemplates.ExecuteTemplate(&buf, templateName, data); err != nil {
|
||||
return "", fmt.Errorf("rendering role template %s: %w", templateName, err)
|
||||
}
|
||||
@@ -131,6 +184,17 @@ func (t *Templates) RoleNames() []string {
|
||||
return []string{"mayor", "witness", "refinery", "polecat", "crew", "deacon"}
|
||||
}
|
||||
|
||||
// HasRoleOverride returns true if the given role has a local override template.
|
||||
func (t *Templates) HasRoleOverride(role string) bool {
|
||||
_, ok := t.roleOverrides[role]
|
||||
return ok
|
||||
}
|
||||
|
||||
// RoleOverrideCount returns the number of role templates that have local overrides.
|
||||
func (t *Templates) RoleOverrideCount() int {
|
||||
return len(t.roleOverrides)
|
||||
}
|
||||
|
||||
// MessageNames returns the list of available message templates.
|
||||
func (t *Templates) MessageNames() []string {
|
||||
return []string{"spawn", "nudge", "escalation", "handoff"}
|
||||
|
||||
Reference in New Issue
Block a user