From a009a2827a9824ad46b077817c6c087ff36d22b4 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 11:09:41 +0000 Subject: [PATCH] feat(agentic): add plan command surface Co-Authored-By: Virgil --- pkg/agentic/commands.go | 1 + pkg/agentic/commands_plan.go | 165 +++++++++++++++++++++++++++++++++++ pkg/agentic/commands_test.go | 61 +++++++++++++ 3 files changed, 227 insertions(+) create mode 100644 pkg/agentic/commands_plan.go diff --git a/pkg/agentic/commands.go b/pkg/agentic/commands.go index db322c3..9061d2a 100644 --- a/pkg/agentic/commands.go +++ b/pkg/agentic/commands.go @@ -26,6 +26,7 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) { c.Command("status", core.Command{Description: "List agent workspace statuses", Action: s.cmdStatus}) c.Command("prompt", core.Command{Description: "Build and display an agent prompt for a repo", Action: s.cmdPrompt}) c.Command("extract", core.Command{Description: "Extract a workspace template to a directory", Action: s.cmdExtract}) + s.registerPlanCommands() } // ctx := s.commandContext() diff --git a/pkg/agentic/commands_plan.go b/pkg/agentic/commands_plan.go new file mode 100644 index 0000000..dd89475 --- /dev/null +++ b/pkg/agentic/commands_plan.go @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + core "dappco.re/go/core" +) + +func (s *PrepSubsystem) registerPlanCommands() { + c := s.Core() + c.Command("plan", core.Command{Description: "Manage implementation plans", Action: s.cmdPlan}) + c.Command("plan/create", core.Command{Description: "Create an implementation plan or create one from a template", Action: s.cmdPlanCreate}) + c.Command("plan/list", core.Command{Description: "List implementation plans", Action: s.cmdPlanList}) + c.Command("plan/show", core.Command{Description: "Show an implementation plan", Action: s.cmdPlanShow}) + c.Command("plan/status", core.Command{Description: "Read or update an implementation plan status", Action: s.cmdPlanStatus}) +} + +func (s *PrepSubsystem) cmdPlan(options core.Options) core.Result { + return s.cmdPlanList(options) +} + +func (s *PrepSubsystem) cmdPlanCreate(options core.Options) core.Result { + ctx := s.commandContext() + slug := optionStringValue(options, "slug", "_arg") + title := optionStringValue(options, "title") + objective := optionStringValue(options, "objective") + description := optionStringValue(options, "description") + templateName := templateNameValue(optionStringValue(options, "template"), optionStringValue(options, "template_slug", "template-slug"), optionStringValue(options, "import")) + + if templateName != "" { + variables := optionStringMapValue(options, "variables") + if variables == nil { + variables = map[string]string{} + } + + _, output, err := s.templateCreatePlan(ctx, nil, TemplateCreatePlanInput{ + Template: templateName, + Variables: variables, + Slug: slug, + Title: title, + Activate: optionBoolValue(options, "activate"), + TemplateSlug: templateName, + }) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "created: %s", output.Plan.Slug) + core.Print(nil, "title: %s", output.Plan.Title) + core.Print(nil, "status: %s", output.Plan.Status) + return core.Result{Value: output, OK: true} + } + + if title == "" { + core.Print(nil, "usage: core-agent plan create --title=\"My Plan\" [--objective=\"...\"] [--description=\"...\"] [--import=bug-fix] [--activate]") + return core.Result{Value: core.E("agentic.cmdPlanCreate", "title is required", nil), OK: false} + } + + if objective == "" { + objective = description + } + if objective == "" { + objective = title + } + + _, output, err := s.planCreate(ctx, nil, PlanCreateInput{ + Title: title, + Slug: slug, + Objective: objective, + Description: description, + Context: optionAnyMapValue(options, "context"), + Repo: optionStringValue(options, "repo"), + Org: optionStringValue(options, "org"), + Phases: planPhasesValue(options, "phases"), + Notes: optionStringValue(options, "notes"), + }) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "created: %s", output.ID) + core.Print(nil, "path: %s", output.Path) + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) cmdPlanList(options core.Options) core.Result { + ctx := s.commandContext() + _, output, err := s.planList(ctx, nil, PlanListInput{ + Status: optionStringValue(options, "status"), + Repo: optionStringValue(options, "repo"), + Limit: optionIntValue(options, "limit"), + }) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + if output.Count == 0 { + core.Print(nil, "no plans") + return core.Result{Value: output, OK: true} + } + + for _, plan := range output.Plans { + core.Print(nil, " %-10s %-24s %s", plan.Status, plan.Slug, plan.Title) + } + core.Print(nil, "%d plan(s)", output.Count) + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) cmdPlanShow(options core.Options) core.Result { + ctx := s.commandContext() + slug := optionStringValue(options, "slug", "_arg") + if slug == "" { + core.Print(nil, "usage: core-agent plan show ") + return core.Result{Value: core.E("agentic.cmdPlanShow", "slug is required", nil), OK: false} + } + + _, output, err := s.planGetCompat(ctx, nil, PlanReadInput{Slug: slug}) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "slug: %s", output.Plan.Slug) + 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.Description != "" { + core.Print(nil, "description: %s", output.Plan.Description) + } + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) cmdPlanStatus(options core.Options) core.Result { + ctx := s.commandContext() + slug := optionStringValue(options, "slug", "_arg") + if slug == "" { + core.Print(nil, "usage: core-agent plan status [--set=ready]") + return core.Result{Value: core.E("agentic.cmdPlanStatus", "slug is required", nil), OK: false} + } + + set := optionStringValue(options, "set", "status") + if set == "" { + _, output, err := s.planGetCompat(ctx, nil, PlanReadInput{Slug: slug}) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + core.Print(nil, "slug: %s", output.Plan.Slug) + core.Print(nil, "status: %s", output.Plan.Status) + return core.Result{Value: output, OK: true} + } + + _, output, err := s.planUpdateStatusCompat(ctx, nil, PlanStatusUpdateInput{Slug: slug, Status: set}) + if err != nil { + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "slug: %s", output.Plan.Slug) + core.Print(nil, "status: %s", output.Plan.Status) + return core.Result{Value: output, OK: true} +} diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index 25bb73d..dcf7257 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -754,6 +754,62 @@ func TestCommands_CmdGenerate_Good_BriefTemplate(t *testing.T) { assert.Contains(t, output, "content: Template draft") } +func TestCommands_CmdPlanCreate_Good(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + + r := s.cmdPlanCreate(core.NewOptions( + core.Option{Key: "slug", Value: "migrate-core"}, + core.Option{Key: "title", Value: "Migrate Core"}, + core.Option{Key: "objective", Value: "Use Core.Process everywhere"}, + )) + + assert.True(t, r.OK) + + output, ok := r.Value.(PlanCreateOutput) + require.True(t, ok) + require.NotEmpty(t, output.ID) + require.NotEmpty(t, output.Path) + assert.True(t, fs.Exists(output.Path)) + + plan, err := readPlan(PlansRoot(), output.ID) + require.NoError(t, err) + assert.Equal(t, "Migrate Core", plan.Title) + assert.Equal(t, "Use Core.Process everywhere", plan.Objective) + assert.Equal(t, "draft", plan.Status) + assert.Equal(t, "migrate-core", plan.Slug) +} + +func TestCommands_CmdPlanStatus_Good_GetAndSet(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + + _, created, err := s.planCreate(context.Background(), nil, PlanCreateInput{ + Title: "Status Plan", + Objective: "Exercise plan status management", + }) + require.NoError(t, err) + + getOutput := captureStdout(t, func() { + r := s.cmdPlanStatus(core.NewOptions(core.Option{Key: "_arg", Value: created.ID})) + assert.True(t, r.OK) + }) + assert.Contains(t, getOutput, "status:") + assert.Contains(t, getOutput, "draft") + + setOutput := captureStdout(t, func() { + r := s.cmdPlanStatus(core.NewOptions( + core.Option{Key: "_arg", Value: created.ID}, + core.Option{Key: "set", Value: "ready"}, + )) + assert.True(t, r.OK) + }) + assert.Contains(t, setOutput, "status:") + assert.Contains(t, setOutput, "ready") + + plan, err := readPlan(PlansRoot(), created.ID) + require.NoError(t, err) + assert.Equal(t, "ready", plan.Status) +} + func TestCommands_CmdExtract_Good(t *testing.T) { s, _ := testPrepWithCore(t, nil) target := core.JoinPath(t.TempDir(), "extract-test") @@ -814,6 +870,11 @@ func TestCommands_RegisterCommands_Good_AllRegistered(t *testing.T) { assert.Contains(t, cmds, "status") assert.Contains(t, cmds, "prompt") assert.Contains(t, cmds, "extract") + assert.Contains(t, cmds, "plan") + assert.Contains(t, cmds, "plan/create") + assert.Contains(t, cmds, "plan/list") + assert.Contains(t, cmds, "plan/show") + assert.Contains(t, cmds, "plan/status") } // --- CmdExtract Bad/Ugly ---