feat(agentic): persist template metadata on plans

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 15:21:03 +00:00
parent 4fff6cc844
commit 534df4278a
5 changed files with 75 additions and 58 deletions

View file

@ -130,6 +130,12 @@ func (s *PrepSubsystem) cmdPlanShow(options core.Options) core.Result {
core.Print(nil, "title: %s", output.Plan.Title)
core.Print(nil, "status: %s", output.Plan.Status)
core.Print(nil, "progress: %d/%d (%d%%)", output.Plan.Progress.Completed, output.Plan.Progress.Total, output.Plan.Progress.Percentage)
if output.Plan.TemplateVersion.Slug != "" {
core.Print(nil, "template: %s v%d", output.Plan.TemplateVersion.Slug, output.Plan.TemplateVersion.Version)
if output.Plan.TemplateVersion.ContentHash != "" {
core.Print(nil, "template id: %s", output.Plan.TemplateVersion.ContentHash)
}
}
if output.Plan.Description != "" {
core.Print(nil, "description: %s", output.Plan.Description)
}

View file

@ -13,21 +13,22 @@ import (
// plan := &Plan{ID: "id-1-a3f2b1", Title: "Migrate Core", Status: "draft", Objective: "Replace raw process calls with Core.Process()"}
// r := writePlanResult(PlansRoot(), plan)
type Plan struct {
ID string `json:"id"`
Slug string `json:"slug,omitempty"`
Title string `json:"title"`
Status string `json:"status"`
Repo string `json:"repo,omitempty"`
Org string `json:"org,omitempty"`
Objective string `json:"objective"`
Description string `json:"description,omitempty"`
Context map[string]any `json:"context,omitempty"`
Phases []Phase `json:"phases,omitempty"`
Notes string `json:"notes,omitempty"`
Agent string `json:"agent,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ArchivedAt time.Time `json:"archived_at,omitempty"`
ID string `json:"id"`
Slug string `json:"slug,omitempty"`
Title string `json:"title"`
Status string `json:"status"`
Repo string `json:"repo,omitempty"`
Org string `json:"org,omitempty"`
Objective string `json:"objective"`
Description string `json:"description,omitempty"`
Context map[string]any `json:"context,omitempty"`
TemplateVersion PlanTemplateVersion `json:"template_version,omitempty"`
Phases []Phase `json:"phases,omitempty"`
Notes string `json:"notes,omitempty"`
Agent string `json:"agent,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ArchivedAt time.Time `json:"archived_at,omitempty"`
}
// phase := agentic.Phase{Number: 1, Name: "Migrate strings", Status: "in_progress"}
@ -63,15 +64,16 @@ type PhaseCheckpoint struct {
}
type PlanCreateInput struct {
Title string `json:"title"`
Slug string `json:"slug,omitempty"`
Objective string `json:"objective,omitempty"`
Description string `json:"description,omitempty"`
Context map[string]any `json:"context,omitempty"`
Repo string `json:"repo,omitempty"`
Org string `json:"org,omitempty"`
Phases []Phase `json:"phases,omitempty"`
Notes string `json:"notes,omitempty"`
Title string `json:"title"`
Slug string `json:"slug,omitempty"`
Objective string `json:"objective,omitempty"`
Description string `json:"description,omitempty"`
Context map[string]any `json:"context,omitempty"`
TemplateVersion PlanTemplateVersion `json:"template_version,omitempty"`
Repo string `json:"repo,omitempty"`
Org string `json:"org,omitempty"`
Phases []Phase `json:"phases,omitempty"`
Notes string `json:"notes,omitempty"`
}
type PlanCreateOutput struct {
@ -305,19 +307,20 @@ func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, in
id := core.ID()
plan := Plan{
ID: id,
Slug: planSlugValue(input.Slug, input.Title, id),
Title: input.Title,
Status: "draft",
Repo: input.Repo,
Org: input.Org,
Objective: objective,
Description: description,
Context: input.Context,
Phases: input.Phases,
Notes: input.Notes,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
ID: id,
Slug: planSlugValue(input.Slug, input.Title, id),
Title: input.Title,
Status: "draft",
Repo: input.Repo,
Org: input.Org,
Objective: objective,
Description: description,
Context: input.Context,
TemplateVersion: input.TemplateVersion,
Phases: input.Phases,
Notes: input.Notes,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
plan = normalisePlan(plan)

View file

@ -46,13 +46,14 @@ type PlanCompatibilityListOutput struct {
// view := agentic.PlanCompatibilityView{Slug: "my-plan-abc123", Title: "My Plan", Status: "active"}
type PlanCompatibilityView struct {
Slug string `json:"slug"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Status string `json:"status"`
Progress PlanProgress `json:"progress"`
Phases []Phase `json:"phases,omitempty"`
Context map[string]any `json:"context,omitempty"`
Slug string `json:"slug"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Status string `json:"status"`
Progress PlanProgress `json:"progress"`
TemplateVersion PlanTemplateVersion `json:"template_version,omitempty"`
Phases []Phase `json:"phases,omitempty"`
Context map[string]any `json:"context,omitempty"`
}
// input := agentic.PlanStatusUpdateInput{Slug: "my-plan-abc123", Status: "active"}
@ -208,13 +209,14 @@ func planCompatibilitySummary(plan Plan) PlanCompatibilitySummary {
func planCompatibilityView(plan Plan) PlanCompatibilityView {
return PlanCompatibilityView{
Slug: plan.Slug,
Title: plan.Title,
Description: plan.Description,
Status: planCompatibilityOutputStatus(plan.Status),
Progress: planProgress(plan),
Phases: plan.Phases,
Context: plan.Context,
Slug: plan.Slug,
Title: plan.Title,
Description: plan.Description,
Status: planCompatibilityOutputStatus(plan.Status),
Progress: planProgress(plan),
TemplateVersion: plan.TemplateVersion,
Phases: plan.Phases,
Context: plan.Context,
}
}

View file

@ -235,13 +235,14 @@ func (s *PrepSubsystem) templateCreatePlan(ctx context.Context, _ *mcp.CallToolR
}
_, created, err := s.planCreate(ctx, nil, PlanCreateInput{
Title: title,
Slug: input.Slug,
Objective: definition.Description,
Description: definition.Description,
Context: contextData,
Phases: templatePlanPhases(definition),
Notes: templateGuidelinesNote(definition.Guidelines),
Title: title,
Slug: input.Slug,
Objective: definition.Description,
Description: definition.Description,
Context: contextData,
TemplateVersion: version,
Phases: templatePlanPhases(definition),
Notes: templateGuidelinesNote(definition.Guidelines),
})
if err != nil {
return nil, TemplateCreatePlanOutput{}, err

View file

@ -111,6 +111,9 @@ func TestTemplate_HandleTemplateCreatePlan_Good(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "Authentication Rollout", plan.Title)
assert.Equal(t, "in_progress", plan.Status)
assert.Equal(t, 1, plan.TemplateVersion.Version)
assert.Equal(t, "new-feature", plan.TemplateVersion.Slug)
assert.NotEmpty(t, plan.TemplateVersion.ContentHash)
require.NotEmpty(t, plan.Phases)
require.NotEmpty(t, plan.Phases[0].Tasks)
assert.Equal(t, "pending", plan.Phases[0].Tasks[0].Status)
@ -137,6 +140,8 @@ func TestTemplate_HandleTemplateCreatePlan_Good_NoVariables(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "api-consistency", stringValue(plan.Context["template"]))
assert.Empty(t, plan.Context["variables"])
assert.Equal(t, 1, plan.TemplateVersion.Version)
assert.Equal(t, "api-consistency", plan.TemplateVersion.Slug)
}
func TestTemplate_HandleTemplateCreatePlan_Bad(t *testing.T) {