feat(agentic): expose plan template snapshot metadata

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 15:12:53 +00:00
parent 6bda31345a
commit 4fff6cc844
3 changed files with 56 additions and 21 deletions

View file

@ -868,7 +868,7 @@ func (s *PrepSubsystem) pullWikiContent(ctx context.Context, org, repo string) s
}
func (s *PrepSubsystem) renderPlan(templateSlug string, variables map[string]string, task string) string {
definition, err := loadPlanTemplateDefinition(templateSlug, variables)
definition, _, err := loadPlanTemplateDefinition(templateSlug, variables)
if err != nil {
return ""
}

View file

@ -4,6 +4,8 @@ package agentic
import (
"context"
"crypto/sha256"
"encoding/hex"
"sort"
"dappco.re/go/agent/pkg/lib"
@ -21,12 +23,13 @@ type TemplateVariable struct {
// summary := agentic.TemplateSummary{Slug: "bug-fix", Name: "Bug Fix", PhasesCount: 6}
type TemplateSummary struct {
Slug string `json:"slug"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Category string `json:"category,omitempty"`
PhasesCount int `json:"phases_count"`
Variables []TemplateVariable `json:"variables,omitempty"`
Slug string `json:"slug"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Category string `json:"category,omitempty"`
PhasesCount int `json:"phases_count"`
Variables []TemplateVariable `json:"variables,omitempty"`
Version PlanTemplateVersion `json:"version"`
}
// input := agentic.TemplateListInput{Category: "development"}
@ -51,9 +54,10 @@ type TemplatePreviewInput struct {
// out := agentic.TemplatePreviewOutput{Success: true, Template: "bug-fix"}
type TemplatePreviewOutput struct {
Success bool `json:"success"`
Template string `json:"template"`
Preview string `json:"preview"`
Success bool `json:"success"`
Template string `json:"template"`
Version PlanTemplateVersion `json:"version"`
Preview string `json:"preview"`
}
// input := agentic.TemplateCreatePlanInput{Template: "new-feature", Variables: map[string]string{"feature_name": "Auth"}}
@ -70,9 +74,18 @@ type TemplateCreatePlanInput struct {
type TemplateCreatePlanOutput struct {
Success bool `json:"success"`
Plan PlanCompatibilitySummary `json:"plan"`
Version PlanTemplateVersion `json:"version"`
Commands map[string]string `json:"commands,omitempty"`
}
// version := agentic.PlanTemplateVersion{Slug: "new-feature", Version: 1, ContentHash: "..." }
type PlanTemplateVersion struct {
Slug string `json:"slug"`
Version int `json:"version"`
Name string `json:"name"`
ContentHash string `json:"content_hash"`
}
type planTemplateDefinition struct {
Slug string `yaml:"-"`
Name string `yaml:"name"`
@ -154,14 +167,14 @@ func (s *PrepSubsystem) registerTemplateTools(server *mcp.Server) {
func (s *PrepSubsystem) templateList(_ context.Context, _ *mcp.CallToolRequest, input TemplateListInput) (*mcp.CallToolResult, TemplateListOutput, error) {
templates := make([]TemplateSummary, 0, len(lib.ListTasks()))
for _, slug := range lib.ListTasks() {
definition, err := loadPlanTemplateDefinition(slug, nil)
definition, version, err := loadPlanTemplateDefinition(slug, nil)
if err != nil {
continue
}
if input.Category != "" && definition.Category != input.Category {
continue
}
templates = append(templates, templateSummaryFromDefinition(definition))
templates = append(templates, templateSummaryFromDefinition(definition, version))
}
sort.Slice(templates, func(i, j int) bool {
@ -181,7 +194,7 @@ func (s *PrepSubsystem) templatePreview(_ context.Context, _ *mcp.CallToolReques
return nil, TemplatePreviewOutput{}, core.E("templatePreview", "template is required", nil)
}
definition, err := loadPlanTemplateDefinition(templateName, input.Variables)
definition, version, err := loadPlanTemplateDefinition(templateName, input.Variables)
if err != nil {
return nil, TemplatePreviewOutput{}, err
}
@ -189,6 +202,7 @@ func (s *PrepSubsystem) templatePreview(_ context.Context, _ *mcp.CallToolReques
return nil, TemplatePreviewOutput{
Success: true,
Template: definition.Slug,
Version: version,
Preview: renderPlanMarkdown(definition, ""),
}, nil
}
@ -199,7 +213,7 @@ func (s *PrepSubsystem) templateCreatePlan(ctx context.Context, _ *mcp.CallToolR
return nil, TemplateCreatePlanOutput{}, core.E("templateCreatePlan", "template is required", nil)
}
definition, err := loadPlanTemplateDefinition(templateName, input.Variables)
definition, version, err := loadPlanTemplateDefinition(templateName, input.Variables)
if err != nil {
return nil, TemplateCreatePlanOutput{}, err
}
@ -252,6 +266,7 @@ func (s *PrepSubsystem) templateCreatePlan(ctx context.Context, _ *mcp.CallToolR
return nil, TemplateCreatePlanOutput{
Success: true,
Plan: planCompatibilitySummary(*plan),
Version: version,
}, nil
}
@ -264,28 +279,29 @@ func templateNameValue(values ...string) string {
return ""
}
func loadPlanTemplateDefinition(slug string, variables map[string]string) (planTemplateDefinition, error) {
func loadPlanTemplateDefinition(slug string, variables map[string]string) (planTemplateDefinition, PlanTemplateVersion, error) {
result := lib.Task(slug)
if !result.OK {
err, _ := result.Value.(error)
if err == nil {
err = core.E("loadPlanTemplateDefinition", core.Concat("template not found: ", slug), nil)
}
return planTemplateDefinition{}, err
return planTemplateDefinition{}, PlanTemplateVersion{}, err
}
content := applyTemplateVariables(result.Value.(string), variables)
rawContent := result.Value.(string)
content := applyTemplateVariables(rawContent, variables)
var definition planTemplateDefinition
if err := yaml.Unmarshal([]byte(content), &definition); err != nil {
return planTemplateDefinition{}, core.E("loadPlanTemplateDefinition", core.Concat("invalid plan template: ", slug), err)
return planTemplateDefinition{}, PlanTemplateVersion{}, core.E("loadPlanTemplateDefinition", core.Concat("invalid plan template: ", slug), err)
}
if definition.Name == "" || len(definition.Phases) == 0 {
return planTemplateDefinition{}, core.E("loadPlanTemplateDefinition", core.Concat("invalid plan template: ", slug), nil)
return planTemplateDefinition{}, PlanTemplateVersion{}, core.E("loadPlanTemplateDefinition", core.Concat("invalid plan template: ", slug), nil)
}
definition.Slug = slug
return definition, nil
return definition, templateVersionFromContent(slug, definition.Name, rawContent), nil
}
func applyTemplateVariables(content string, variables map[string]string) string {
@ -328,7 +344,7 @@ func renderPlanMarkdown(definition planTemplateDefinition, task string) string {
return plan.String()
}
func templateSummaryFromDefinition(definition planTemplateDefinition) TemplateSummary {
func templateSummaryFromDefinition(definition planTemplateDefinition, version PlanTemplateVersion) TemplateSummary {
return TemplateSummary{
Slug: definition.Slug,
Name: definition.Name,
@ -336,6 +352,17 @@ func templateSummaryFromDefinition(definition planTemplateDefinition) TemplateSu
Category: definition.Category,
PhasesCount: len(definition.Phases),
Variables: templateVariableList(definition),
Version: version,
}
}
func templateVersionFromContent(slug, name, content string) PlanTemplateVersion {
sum := sha256.Sum256([]byte(content))
return PlanTemplateVersion{
Slug: slug,
Version: 1,
Name: name,
ContentHash: hex.EncodeToString(sum[:]),
}
}

View file

@ -26,6 +26,8 @@ func TestTemplate_HandleTemplateList_Good(t *testing.T) {
found := false
for _, summary := range output.Templates {
assert.Equal(t, "development", summary.Category)
assert.Equal(t, 1, summary.Version.Version)
assert.NotEmpty(t, summary.Version.ContentHash)
if summary.Slug == "bug-fix" {
found = true
assert.Equal(t, "Bug Fix", summary.Name)
@ -48,6 +50,8 @@ func TestTemplate_HandleTemplatePreview_Good(t *testing.T) {
require.True(t, ok)
assert.True(t, output.Success)
assert.Equal(t, "new-feature", output.Template)
assert.Equal(t, 1, output.Version.Version)
assert.NotEmpty(t, output.Version.ContentHash)
assert.Contains(t, output.Preview, "Authentication")
assert.Contains(t, output.Preview, "Phase 1")
}
@ -100,6 +104,8 @@ func TestTemplate_HandleTemplateCreatePlan_Good(t *testing.T) {
assert.True(t, output.Success)
assert.Equal(t, "auth-rollout", output.Plan.Slug)
assert.Equal(t, "active", output.Plan.Status)
assert.Equal(t, 1, output.Version.Version)
assert.NotEmpty(t, output.Version.ContentHash)
plan, err := readPlan(PlansRoot(), "auth-rollout")
require.NoError(t, err)
@ -124,6 +130,8 @@ func TestTemplate_HandleTemplateCreatePlan_Good_NoVariables(t *testing.T) {
assert.NotEmpty(t, output.Plan.Slug)
assert.Equal(t, "API Consistency Audit", output.Plan.Title)
assert.Equal(t, "draft", output.Plan.Status)
assert.Equal(t, 1, output.Version.Version)
assert.NotEmpty(t, output.Version.ContentHash)
plan, err := readPlan(PlansRoot(), output.Plan.Slug)
require.NoError(t, err)