From 689b2e90e566db3abdfc9000c37a9e80a336fe0a Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 01:27:50 +0000 Subject: [PATCH] feat(agentic): expose plan update CLI command Co-Authored-By: Virgil --- pkg/agentic/commands_plan.go | 50 +++++++++++++++++++++++++++++++ pkg/agentic/commands_plan_test.go | 40 +++++++++++++++++++++++++ pkg/agentic/commands_test.go | 2 ++ 3 files changed, 92 insertions(+) diff --git a/pkg/agentic/commands_plan.go b/pkg/agentic/commands_plan.go index d8a97b2..5e53820 100644 --- a/pkg/agentic/commands_plan.go +++ b/pkg/agentic/commands_plan.go @@ -18,6 +18,8 @@ func (s *PrepSubsystem) registerPlanCommands() { c.Command("agentic:plan/read", core.Command{Description: "Read an implementation plan", Action: s.cmdPlanShow}) c.Command("plan/read", core.Command{Description: "Read an implementation plan", Action: s.cmdPlanShow}) c.Command("plan/show", core.Command{Description: "Show an implementation plan", Action: s.cmdPlanShow}) + c.Command("plan/update", core.Command{Description: "Update an implementation plan", Action: s.cmdPlanUpdate}) + c.Command("agentic:plan/update", core.Command{Description: "Update an implementation plan", Action: s.cmdPlanUpdate}) c.Command("plan/status", core.Command{Description: "Read or update an implementation plan status", Action: s.cmdPlanStatus}) c.Command("plan/check", core.Command{Description: "Check whether a plan or phase is complete", Action: s.cmdPlanCheck}) c.Command("plan/archive", core.Command{Description: "Archive an implementation plan by slug or ID", Action: s.cmdPlanArchive}) @@ -174,6 +176,54 @@ func (s *PrepSubsystem) cmdPlanShow(options core.Options) core.Result { return core.Result{Value: output, OK: true} } +func (s *PrepSubsystem) cmdPlanUpdate(options core.Options) core.Result { + ctx := s.commandContext() + id := optionStringValue(options, "id", "_arg") + slug := optionStringValue(options, "slug") + hasChanges := options.Has("status") || options.Has("title") || options.Has("objective") || options.Has("description") || options.Has("notes") || options.Has("agent") || options.Has("context") || options.Has("phases") + if id == "" && slug == "" { + core.Print(nil, "usage: core-agent plan update [--status=ready] [--title=\"...\"] [--objective=\"...\"] [--description=\"...\"] [--notes=\"...\"] [--agent=codex] [--context='{\"repo\":\"go-io\"}'] [--phases='[...]']") + return core.Result{Value: core.E("agentic.cmdPlanUpdate", "id or slug is required", nil), OK: false} + } + if !hasChanges { + core.Print(nil, "usage: core-agent plan update [--status=ready] [--title=\"...\"] [--objective=\"...\"] [--description=\"...\"] [--notes=\"...\"] [--agent=codex] [--context='{\"repo\":\"go-io\"}'] [--phases='[...]']") + return core.Result{Value: core.E("agentic.cmdPlanUpdate", "at least one update field is required", nil), OK: false} + } + + result := s.handlePlanUpdate(ctx, core.NewOptions( + core.Option{Key: "id", Value: id}, + core.Option{Key: "slug", Value: slug}, + core.Option{Key: "status", Value: optionStringValue(options, "status")}, + core.Option{Key: "title", Value: optionStringValue(options, "title")}, + core.Option{Key: "objective", Value: optionStringValue(options, "objective")}, + core.Option{Key: "description", Value: optionStringValue(options, "description")}, + core.Option{Key: "context", Value: optionAnyMapValue(options, "context")}, + core.Option{Key: "phases", Value: planPhasesValue(options, "phases")}, + core.Option{Key: "notes", Value: optionStringValue(options, "notes")}, + core.Option{Key: "agent", Value: optionStringValue(options, "agent")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdPlanUpdate", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(PlanUpdateOutput) + if !ok { + err := core.E("agentic.cmdPlanUpdate", "invalid plan update output", 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) + if output.Plan.Agent != "" { + core.Print(nil, "agent: %s", output.Plan.Agent) + } + return core.Result{Value: output, OK: true} +} + func (s *PrepSubsystem) cmdPlanStatus(options core.Options) core.Result { ctx := s.commandContext() slug := optionStringValue(options, "slug", "_arg") diff --git a/pkg/agentic/commands_plan_test.go b/pkg/agentic/commands_plan_test.go index 8cd0378..38ef6dd 100644 --- a/pkg/agentic/commands_plan_test.go +++ b/pkg/agentic/commands_plan_test.go @@ -92,6 +92,44 @@ func TestCommandsPlan_CmdPlanCheck_Ugly_IncompletePhase(t *testing.T) { assert.Equal(t, []string{"Patch code"}, output.Pending) } +func TestCommandsPlan_CmdPlanUpdate_Good_StatusAndAgent(t *testing.T) { + dir := t.TempDir() + t.Setenv("CORE_WORKSPACE", dir) + + s := newTestPrep(t) + _, created, err := s.planCreate(context.Background(), nil, PlanCreateInput{ + Title: "Update Command", + Objective: "Verify the plan update command", + }) + require.NoError(t, err) + + r := s.cmdPlanUpdate(core.NewOptions( + core.Option{Key: "_arg", Value: created.ID}, + core.Option{Key: "status", Value: "ready"}, + core.Option{Key: "agent", Value: "codex"}, + )) + require.True(t, r.OK) + + output, ok := r.Value.(PlanUpdateOutput) + require.True(t, ok) + assert.True(t, output.Success) + assert.Equal(t, created.ID, output.Plan.ID) + assert.Equal(t, "ready", output.Plan.Status) + assert.Equal(t, "codex", output.Plan.Agent) +} + +func TestCommandsPlan_CmdPlanUpdate_Bad_MissingFields(t *testing.T) { + s := newTestPrep(t) + + r := s.cmdPlanUpdate(core.NewOptions( + core.Option{Key: "_arg", Value: "plan-123"}, + )) + + assert.False(t, r.OK) + require.Error(t, r.Value.(error)) + assert.Contains(t, r.Value.(error).Error(), "at least one update field is required") +} + func TestCommandsPlan_HandlePlanCheck_Good_CompletePlan(t *testing.T) { dir := t.TempDir() t.Setenv("CORE_WORKSPACE", dir) @@ -139,4 +177,6 @@ func TestCommandsPlan_RegisterPlanCommands_Good_SpecAliasRegistered(t *testing.T assert.Contains(t, c.Commands(), "agentic:plan/read") assert.Contains(t, c.Commands(), "plan/read") assert.Contains(t, c.Commands(), "plan/show") + assert.Contains(t, c.Commands(), "plan/update") + assert.Contains(t, c.Commands(), "agentic:plan/update") } diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index e65eec4..d92a1a4 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -1416,6 +1416,8 @@ func TestCommands_RegisterCommands_Good_AllRegistered(t *testing.T) { assert.Contains(t, cmds, "plan/list") assert.Contains(t, cmds, "plan/read") assert.Contains(t, cmds, "plan/show") + assert.Contains(t, cmds, "plan/update") + assert.Contains(t, cmds, "agentic:plan/update") assert.Contains(t, cmds, "plan/status") assert.Contains(t, cmds, "plan/check") assert.Contains(t, cmds, "plan/archive")