diff --git a/pkg/agentic/commands.go b/pkg/agentic/commands.go index 99545e3..7196226 100644 --- a/pkg/agentic/commands.go +++ b/pkg/agentic/commands.go @@ -28,6 +28,7 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) { 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() + s.registerTaskCommands() } // ctx := s.commandContext() diff --git a/pkg/agentic/commands_task.go b/pkg/agentic/commands_task.go new file mode 100644 index 0000000..e849ab8 --- /dev/null +++ b/pkg/agentic/commands_task.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + core "dappco.re/go/core" +) + +func (s *PrepSubsystem) registerTaskCommands() { + c := s.Core() + c.Command("task", core.Command{Description: "Manage plan tasks", Action: s.cmdTask}) + c.Command("task/update", core.Command{Description: "Update a plan task status or notes", Action: s.cmdTaskUpdate}) + c.Command("task/toggle", core.Command{Description: "Toggle a plan task between pending and completed", Action: s.cmdTaskToggle}) +} + +func (s *PrepSubsystem) cmdTask(options core.Options) core.Result { + action := optionStringValue(options, "action") + switch action { + case "toggle": + return s.cmdTaskToggle(options) + case "update": + return s.cmdTaskUpdate(options) + case "": + core.Print(nil, "usage: core-agent task update --phase=1 --task=1 [--status=completed] [--notes=\"Done\"]") + core.Print(nil, " core-agent task toggle --phase=1 --task=1") + return core.Result{OK: true} + default: + core.Print(nil, "usage: core-agent task update --phase=1 --task=1 [--status=completed] [--notes=\"Done\"]") + core.Print(nil, " core-agent task toggle --phase=1 --task=1") + return core.Result{Value: core.E("agentic.cmdTask", core.Concat("unknown task command: ", action), nil), OK: false} + } +} + +func (s *PrepSubsystem) cmdTaskUpdate(options core.Options) core.Result { + planSlug := optionStringValue(options, "plan_slug", "plan", "slug", "_arg") + phaseOrder := optionIntValue(options, "phase_order", "phase") + taskIdentifier := optionAnyValue(options, "task_identifier", "task") + + if planSlug == "" || phaseOrder == 0 || taskIdentifierValue(taskIdentifier) == "" { + core.Print(nil, "usage: core-agent task update --phase=1 --task=1 [--status=completed] [--notes=\"Done\"]") + return core.Result{Value: core.E("agentic.cmdTaskUpdate", "plan_slug, phase_order, and task_identifier are required", nil), OK: false} + } + + result := s.handleTaskUpdate(s.commandContext(), core.NewOptions( + core.Option{Key: "plan_slug", Value: planSlug}, + core.Option{Key: "phase_order", Value: phaseOrder}, + core.Option{Key: "task_identifier", Value: taskIdentifier}, + core.Option{Key: "status", Value: optionStringValue(options, "status")}, + core.Option{Key: "notes", Value: optionStringValue(options, "notes")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdTaskUpdate", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(TaskOutput) + if !ok { + err := core.E("agentic.cmdTaskUpdate", "invalid task update output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "task: %s", output.Task.Title) + core.Print(nil, "status: %s", output.Task.Status) + if output.Task.Notes != "" { + core.Print(nil, "notes: %s", output.Task.Notes) + } + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) cmdTaskToggle(options core.Options) core.Result { + planSlug := optionStringValue(options, "plan_slug", "plan", "slug", "_arg") + phaseOrder := optionIntValue(options, "phase_order", "phase") + taskIdentifier := optionAnyValue(options, "task_identifier", "task") + + if planSlug == "" || phaseOrder == 0 || taskIdentifierValue(taskIdentifier) == "" { + core.Print(nil, "usage: core-agent task toggle --phase=1 --task=1") + return core.Result{Value: core.E("agentic.cmdTaskToggle", "plan_slug, phase_order, and task_identifier are required", nil), OK: false} + } + + result := s.handleTaskToggle(s.commandContext(), core.NewOptions( + core.Option{Key: "plan_slug", Value: planSlug}, + core.Option{Key: "phase_order", Value: phaseOrder}, + core.Option{Key: "task_identifier", Value: taskIdentifier}, + )) + if !result.OK { + err := commandResultError("agentic.cmdTaskToggle", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(TaskOutput) + if !ok { + err := core.E("agentic.cmdTaskToggle", "invalid task toggle output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "task: %s", output.Task.Title) + core.Print(nil, "status: %s", output.Task.Status) + return core.Result{Value: output, OK: true} +} diff --git a/pkg/agentic/commands_task_test.go b/pkg/agentic/commands_task_test.go new file mode 100644 index 0000000..acc36ab --- /dev/null +++ b/pkg/agentic/commands_task_test.go @@ -0,0 +1,86 @@ +// 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 TestCommands_TaskCommand_Good_Update(t *testing.T) { + dir := t.TempDir() + t.Setenv("CORE_WORKSPACE", dir) + + s := newTestPrep(t) + _, created, err := s.planCreate(context.Background(), nil, PlanCreateInput{ + Title: "Task Command", + Description: "Update task through CLI command", + Phases: []Phase{ + {Name: "Setup", Tasks: []PlanTask{{ID: "1", Title: "Review RFC"}}}, + }, + }) + require.NoError(t, err) + + plan, err := readPlan(PlansRoot(), created.ID) + require.NoError(t, err) + + r := s.cmdTaskUpdate(core.NewOptions( + core.Option{Key: "plan_slug", Value: plan.Slug}, + core.Option{Key: "phase_order", Value: 1}, + core.Option{Key: "task_identifier", Value: "1"}, + core.Option{Key: "status", Value: "completed"}, + core.Option{Key: "notes", Value: "Done"}, + )) + require.True(t, r.OK) + + output, ok := r.Value.(TaskOutput) + require.True(t, ok) + assert.Equal(t, "completed", output.Task.Status) + assert.Equal(t, "Done", output.Task.Notes) +} + +func TestCommands_TaskCommand_Bad_MissingRequiredFields(t *testing.T) { + s := newTestPrep(t) + + r := s.cmdTaskUpdate(core.NewOptions( + core.Option{Key: "phase_order", Value: 1}, + core.Option{Key: "task_identifier", Value: "1"}, + )) + + assert.False(t, r.OK) + assert.Contains(t, r.Value.(error).Error(), "required") +} + +func TestCommands_TaskCommand_Ugly_ToggleCriteriaFallback(t *testing.T) { + dir := t.TempDir() + t.Setenv("CORE_WORKSPACE", dir) + + s := newTestPrep(t) + _, created, err := s.planCreate(context.Background(), nil, PlanCreateInput{ + Title: "Task Toggle", + Description: "Toggle criteria-derived task", + Phases: []Phase{ + {Name: "Setup", Criteria: []string{"Review RFC"}}, + }, + }) + require.NoError(t, err) + + plan, err := readPlan(PlansRoot(), created.ID) + require.NoError(t, err) + + r := s.cmdTaskToggle(core.NewOptions( + core.Option{Key: "plan_slug", Value: plan.Slug}, + core.Option{Key: "phase_order", Value: 1}, + core.Option{Key: "task_identifier", Value: 1}, + )) + require.True(t, r.OK) + + output, ok := r.Value.(TaskOutput) + require.True(t, ok) + assert.Equal(t, "completed", output.Task.Status) + assert.Equal(t, "Review RFC", output.Task.Title) +} diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index dcf7257..4d14c2f 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -875,6 +875,9 @@ func TestCommands_RegisterCommands_Good_AllRegistered(t *testing.T) { assert.Contains(t, cmds, "plan/list") assert.Contains(t, cmds, "plan/show") assert.Contains(t, cmds, "plan/status") + assert.Contains(t, cmds, "task") + assert.Contains(t, cmds, "task/update") + assert.Contains(t, cmds, "task/toggle") } // --- CmdExtract Bad/Ugly --- diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index cddaa1e..72477b5 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -583,6 +583,9 @@ func TestPrep_OnStartup_Good_RegistersGenerateCommand(t *testing.T) { assert.Contains(t, c.Commands(), "brain/ingest") assert.Contains(t, c.Commands(), "brain/seed-memory") assert.Contains(t, c.Commands(), "plan-cleanup") + assert.Contains(t, c.Commands(), "task") + assert.Contains(t, c.Commands(), "task/update") + assert.Contains(t, c.Commands(), "task/toggle") } func TestPrep_OnStartup_Bad(t *testing.T) {