diff --git a/pkg/agentic/commands.go b/pkg/agentic/commands.go index 97aaa18..b566060 100644 --- a/pkg/agentic/commands.go +++ b/pkg/agentic/commands.go @@ -85,6 +85,7 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) { s.registerPlanCommands() s.registerCommitCommands() s.registerSessionCommands() + s.registerPhaseCommands() s.registerTaskCommands() s.registerSprintCommands() s.registerStateCommands() diff --git a/pkg/agentic/commands_phase.go b/pkg/agentic/commands_phase.go new file mode 100644 index 0000000..c97f749 --- /dev/null +++ b/pkg/agentic/commands_phase.go @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + core "dappco.re/go/core" +) + +func (s *PrepSubsystem) registerPhaseCommands() { + c := s.Core() + c.Command("phase", core.Command{Description: "Manage plan phases", Action: s.cmdPhase}) + c.Command("agentic:phase", core.Command{Description: "Manage plan phases", Action: s.cmdPhase}) + c.Command("phase/get", core.Command{Description: "Read a plan phase by slug and order", Action: s.cmdPhaseGet}) + c.Command("agentic:phase/get", core.Command{Description: "Read a plan phase by slug and order", Action: s.cmdPhaseGet}) + c.Command("phase/update_status", core.Command{Description: "Update a plan phase status by slug and order", Action: s.cmdPhaseUpdateStatus}) + c.Command("agentic:phase/update_status", core.Command{Description: "Update a plan phase status by slug and order", Action: s.cmdPhaseUpdateStatus}) + c.Command("phase/update-status", core.Command{Description: "Update a plan phase status by slug and order", Action: s.cmdPhaseUpdateStatus}) + c.Command("agentic:phase/update-status", core.Command{Description: "Update a plan phase status by slug and order", Action: s.cmdPhaseUpdateStatus}) + c.Command("phase/add_checkpoint", core.Command{Description: "Append a checkpoint note to a plan phase", Action: s.cmdPhaseAddCheckpoint}) + c.Command("agentic:phase/add_checkpoint", core.Command{Description: "Append a checkpoint note to a plan phase", Action: s.cmdPhaseAddCheckpoint}) + c.Command("phase/add-checkpoint", core.Command{Description: "Append a checkpoint note to a plan phase", Action: s.cmdPhaseAddCheckpoint}) + c.Command("agentic:phase/add-checkpoint", core.Command{Description: "Append a checkpoint note to a plan phase", Action: s.cmdPhaseAddCheckpoint}) +} + +func (s *PrepSubsystem) cmdPhase(options core.Options) core.Result { + switch action := optionStringValue(options, "action"); action { + case "get": + return s.cmdPhaseGet(options) + case "update_status", "update-status", "update": + return s.cmdPhaseUpdateStatus(options) + case "add_checkpoint", "add-checkpoint", "checkpoint": + return s.cmdPhaseAddCheckpoint(options) + case "": + core.Print(nil, "usage: core-agent phase get --phase=1") + core.Print(nil, " core-agent phase update-status --phase=1 --status=completed [--reason=\"...\"]") + core.Print(nil, " core-agent phase add-checkpoint --phase=1 --note=\"Build passes\" [--context='{\"build\":\"ok\"}']") + return core.Result{OK: true} + default: + core.Print(nil, "usage: core-agent phase get --phase=1") + core.Print(nil, " core-agent phase update-status --phase=1 --status=completed [--reason=\"...\"]") + core.Print(nil, " core-agent phase add-checkpoint --phase=1 --note=\"Build passes\" [--context='{\"build\":\"ok\"}']") + return core.Result{Value: core.E("agentic.cmdPhase", core.Concat("unknown phase command: ", action), nil), OK: false} + } +} + +func (s *PrepSubsystem) cmdPhaseGet(options core.Options) core.Result { + result := s.handlePhaseGet(s.commandContext(), core.NewOptions( + core.Option{Key: "plan_slug", Value: optionStringValue(options, "plan_slug", "plan", "slug", "_arg")}, + core.Option{Key: "phase_order", Value: optionIntValue(options, "phase_order", "phase")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdPhaseGet", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(PhaseOutput) + if !ok { + err := core.E("agentic.cmdPhaseGet", "invalid phase get output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "phase: %d", output.Phase.Number) + core.Print(nil, "name: %s", output.Phase.Name) + core.Print(nil, "status: %s", output.Phase.Status) + if output.Phase.Description != "" { + core.Print(nil, "desc: %s", output.Phase.Description) + } + if output.Phase.Notes != "" { + core.Print(nil, "notes: %s", output.Phase.Notes) + } + if len(output.Phase.Tasks) > 0 { + core.Print(nil, "tasks: %d", len(output.Phase.Tasks)) + } + if len(output.Phase.Checkpoints) > 0 { + core.Print(nil, "checkpoints: %d", len(output.Phase.Checkpoints)) + } + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) cmdPhaseUpdateStatus(options core.Options) core.Result { + result := s.handlePhaseUpdateStatus(s.commandContext(), core.NewOptions( + core.Option{Key: "plan_slug", Value: optionStringValue(options, "plan_slug", "plan", "slug", "_arg")}, + core.Option{Key: "phase_order", Value: optionIntValue(options, "phase_order", "phase")}, + core.Option{Key: "status", Value: optionStringValue(options, "status")}, + core.Option{Key: "reason", Value: optionStringValue(options, "reason")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdPhaseUpdateStatus", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(PhaseOutput) + if !ok { + err := core.E("agentic.cmdPhaseUpdateStatus", "invalid phase update output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "phase: %d", output.Phase.Number) + core.Print(nil, "name: %s", output.Phase.Name) + core.Print(nil, "status: %s", output.Phase.Status) + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) cmdPhaseAddCheckpoint(options core.Options) core.Result { + result := s.handlePhaseAddCheckpoint(s.commandContext(), core.NewOptions( + core.Option{Key: "plan_slug", Value: optionStringValue(options, "plan_slug", "plan", "slug", "_arg")}, + core.Option{Key: "phase_order", Value: optionIntValue(options, "phase_order", "phase")}, + core.Option{Key: "note", Value: optionStringValue(options, "note")}, + core.Option{Key: "context", Value: optionAnyMapValue(options, "context")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdPhaseAddCheckpoint", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(PhaseOutput) + if !ok { + err := core.E("agentic.cmdPhaseAddCheckpoint", "invalid phase checkpoint output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "phase: %d", output.Phase.Number) + core.Print(nil, "name: %s", output.Phase.Name) + core.Print(nil, "status: %s", output.Phase.Status) + core.Print(nil, "checkpoints: %d", len(output.Phase.Checkpoints)) + return core.Result{Value: output, OK: true} +} diff --git a/pkg/agentic/commands_phase_test.go b/pkg/agentic/commands_phase_test.go new file mode 100644 index 0000000..0651f92 --- /dev/null +++ b/pkg/agentic/commands_phase_test.go @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "context" + "testing" + + core "dappco.re/go/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCommandsPhase_RegisterPhaseCommands_Good_AllRegistered(t *testing.T) { + s, c := testPrepWithCore(t, nil) + s.registerPhaseCommands() + + cmds := c.Commands() + assert.Contains(t, cmds, "phase") + assert.Contains(t, cmds, "agentic:phase") + assert.Contains(t, cmds, "phase/get") + assert.Contains(t, cmds, "agentic:phase/get") + assert.Contains(t, cmds, "phase/update_status") + assert.Contains(t, cmds, "agentic:phase/update_status") + assert.Contains(t, cmds, "phase/update-status") + assert.Contains(t, cmds, "agentic:phase/update-status") + assert.Contains(t, cmds, "phase/add_checkpoint") + assert.Contains(t, cmds, "agentic:phase/add_checkpoint") + assert.Contains(t, cmds, "phase/add-checkpoint") + assert.Contains(t, cmds, "agentic:phase/add-checkpoint") +} + +func TestCommandsPhase_CmdPhase_Good_GetUpdateCheckpoint(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + _, _, err := s.planCreate(context.Background(), nil, PlanCreateInput{ + Title: "Phase command plan", + Slug: "phase-command-plan", + Objective: "Exercise phase commands", + Phases: []Phase{ + {Number: 1, Name: "Setup", Status: "pending"}, + }, + }) + require.NoError(t, err) + + output := captureStdout(t, func() { + r := s.cmdPhase(core.NewOptions( + core.Option{Key: "action", Value: "get"}, + core.Option{Key: "_arg", Value: "phase-command-plan"}, + core.Option{Key: "phase", Value: 1}, + )) + assert.True(t, r.OK) + }) + assert.Contains(t, output, "phase: 1") + assert.Contains(t, output, "name: Setup") + assert.Contains(t, output, "status: pending") + + output = captureStdout(t, func() { + r := s.cmdPhase(core.NewOptions( + core.Option{Key: "action", Value: "update-status"}, + core.Option{Key: "_arg", Value: "phase-command-plan"}, + core.Option{Key: "phase", Value: 1}, + core.Option{Key: "status", Value: "completed"}, + )) + assert.True(t, r.OK) + }) + assert.Contains(t, output, "status: completed") + + output = captureStdout(t, func() { + r := s.cmdPhase(core.NewOptions( + core.Option{Key: "action", Value: "add-checkpoint"}, + core.Option{Key: "_arg", Value: "phase-command-plan"}, + core.Option{Key: "phase", Value: 1}, + core.Option{Key: "note", Value: "Build passes"}, + )) + assert.True(t, r.OK) + }) + assert.Contains(t, output, "checkpoints: 1") +} + +func TestCommandsPhase_CmdPhase_Bad_MissingActionStillShowsUsage(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + output := captureStdout(t, func() { + r := s.cmdPhase(core.NewOptions()) + assert.True(t, r.OK) + }) + assert.Contains(t, output, "core-agent phase get") +} + +func TestCommandsPhase_CmdPhase_Ugly_UnknownAction(t *testing.T) { + s, _ := testPrepWithCore(t, nil) + r := s.cmdPhase(core.NewOptions(core.Option{Key: "action", Value: "explode"})) + require.False(t, r.OK) + assert.Contains(t, r.Value.(error).Error(), "unknown phase command") +} diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index 088fde6..07dae10 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -1614,6 +1614,14 @@ func TestCommands_RegisterCommands_Good_AllRegistered(t *testing.T) { assert.Contains(t, cmds, "review-queue") assert.Contains(t, cmds, "agentic:review-queue") assert.Contains(t, cmds, "task") + assert.Contains(t, cmds, "phase") + assert.Contains(t, cmds, "agentic:phase") + assert.Contains(t, cmds, "phase/get") + assert.Contains(t, cmds, "agentic:phase/get") + assert.Contains(t, cmds, "phase/update_status") + assert.Contains(t, cmds, "agentic:phase/update_status") + assert.Contains(t, cmds, "phase/add_checkpoint") + assert.Contains(t, cmds, "agentic:phase/add_checkpoint") assert.Contains(t, cmds, "task/update") assert.Contains(t, cmds, "task/toggle") assert.Contains(t, cmds, "sprint") diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index 71eafae..68437a0 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -754,6 +754,14 @@ func TestPrep_OnStartup_Good_RegistersGenerateCommand(t *testing.T) { assert.Contains(t, c.Commands(), "task/create") assert.Contains(t, c.Commands(), "task/update") assert.Contains(t, c.Commands(), "task/toggle") + assert.Contains(t, c.Commands(), "phase") + assert.Contains(t, c.Commands(), "agentic:phase") + assert.Contains(t, c.Commands(), "phase/get") + assert.Contains(t, c.Commands(), "agentic:phase/get") + assert.Contains(t, c.Commands(), "phase/update_status") + assert.Contains(t, c.Commands(), "agentic:phase/update_status") + assert.Contains(t, c.Commands(), "phase/add_checkpoint") + assert.Contains(t, c.Commands(), "agentic:phase/add_checkpoint") assert.Contains(t, c.Commands(), "state") assert.Contains(t, c.Commands(), "state/set") assert.Contains(t, c.Commands(), "state/get")