feat(agentic): add plan check command
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
c55f4e4f5e
commit
460af585ed
4 changed files with 214 additions and 0 deletions
|
|
@ -13,6 +13,7 @@ func (s *PrepSubsystem) registerPlanCommands() {
|
|||
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})
|
||||
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})
|
||||
c.Command("plan/delete", core.Command{Description: "Delete an implementation plan by ID", Action: s.cmdPlanDelete})
|
||||
}
|
||||
|
|
@ -166,6 +167,46 @@ func (s *PrepSubsystem) cmdPlanStatus(options core.Options) core.Result {
|
|||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) cmdPlanCheck(options core.Options) core.Result {
|
||||
ctx := s.commandContext()
|
||||
slug := optionStringValue(options, "slug", "_arg")
|
||||
if slug == "" {
|
||||
core.Print(nil, "usage: core-agent plan check <slug> [--phase=1]")
|
||||
return core.Result{Value: core.E("agentic.cmdPlanCheck", "slug is required", nil), OK: false}
|
||||
}
|
||||
|
||||
phaseOrder := optionIntValue(options, "phase", "phase_order")
|
||||
_, 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}
|
||||
}
|
||||
|
||||
check := planCheckOutput(output.Plan, phaseOrder)
|
||||
core.Print(nil, "slug: %s", check.Plan.Slug)
|
||||
core.Print(nil, "status: %s", check.Plan.Status)
|
||||
core.Print(nil, "progress: %d/%d (%d%%)", check.Plan.Progress.Completed, check.Plan.Progress.Total, check.Plan.Progress.Percentage)
|
||||
if check.Phase > 0 {
|
||||
core.Print(nil, "phase: %d %s", check.Phase, check.PhaseName)
|
||||
}
|
||||
if len(check.Pending) > 0 {
|
||||
core.Print(nil, "pending:")
|
||||
for _, item := range check.Pending {
|
||||
core.Print(nil, " - %s", item)
|
||||
}
|
||||
}
|
||||
if check.Complete {
|
||||
core.Print(nil, "complete")
|
||||
} else {
|
||||
core.Print(nil, "incomplete")
|
||||
}
|
||||
|
||||
if !check.Complete {
|
||||
return core.Result{Value: check, OK: false}
|
||||
}
|
||||
return core.Result{Value: check, OK: true}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) cmdPlanArchive(options core.Options) core.Result {
|
||||
ctx := s.commandContext()
|
||||
id := optionStringValue(options, "id", "slug", "_arg")
|
||||
|
|
@ -223,3 +264,72 @@ func (s *PrepSubsystem) cmdPlanDelete(options core.Options) core.Result {
|
|||
core.Print(nil, "deleted: %s", output.Deleted)
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
func planCheckOutput(plan PlanCompatibilityView, phaseOrder int) PlanCheckOutput {
|
||||
output := PlanCheckOutput{
|
||||
Success: true,
|
||||
Plan: plan,
|
||||
}
|
||||
|
||||
if phaseOrder <= 0 {
|
||||
output.Complete, output.Pending = planCompleteOutput(plan.Phases)
|
||||
return output
|
||||
}
|
||||
|
||||
for _, phase := range plan.Phases {
|
||||
if phase.Number != phaseOrder {
|
||||
continue
|
||||
}
|
||||
output.Phase = phase.Number
|
||||
output.PhaseName = phase.Name
|
||||
output.Complete, output.Pending = phaseCompleteOutput(phase)
|
||||
return output
|
||||
}
|
||||
|
||||
output.Complete = false
|
||||
output.Pending = []string{core.Concat("phase ", core.Sprint(phaseOrder), " not found")}
|
||||
return output
|
||||
}
|
||||
|
||||
func planCompleteOutput(phases []Phase) (bool, []string) {
|
||||
var pending []string
|
||||
for _, phase := range phases {
|
||||
phaseComplete, phasePending := phaseCompleteOutput(phase)
|
||||
if phaseComplete {
|
||||
continue
|
||||
}
|
||||
if len(phasePending) == 0 {
|
||||
pending = append(pending, core.Concat("phase ", core.Sprint(phase.Number), ": ", phase.Name))
|
||||
continue
|
||||
}
|
||||
for _, item := range phasePending {
|
||||
pending = append(pending, core.Concat("phase ", core.Sprint(phase.Number), ": ", item))
|
||||
}
|
||||
}
|
||||
return len(pending) == 0, pending
|
||||
}
|
||||
|
||||
func phaseCompleteOutput(phase Phase) (bool, []string) {
|
||||
tasks := phaseTaskList(phase)
|
||||
if len(tasks) == 0 {
|
||||
switch phase.Status {
|
||||
case "completed", "done", "approved":
|
||||
return true, nil
|
||||
default:
|
||||
return false, []string{phase.Name}
|
||||
}
|
||||
}
|
||||
|
||||
var pending []string
|
||||
for _, task := range tasks {
|
||||
if task.Status == "completed" {
|
||||
continue
|
||||
}
|
||||
label := task.Title
|
||||
if label == "" {
|
||||
label = task.ID
|
||||
}
|
||||
pending = append(pending, label)
|
||||
}
|
||||
return len(pending) == 0, pending
|
||||
}
|
||||
|
|
|
|||
93
pkg/agentic/commands_plan_test.go
Normal file
93
pkg/agentic/commands_plan_test.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// 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 TestCommandsPlan_CmdPlanCheck_Good_CompletePlan(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
_, created, err := s.planCreate(context.Background(), nil, PlanCreateInput{
|
||||
Title: "Check Plan",
|
||||
Description: "Confirm the plan check command reports completion",
|
||||
Phases: []Phase{
|
||||
{
|
||||
Name: "Setup",
|
||||
Tasks: []PlanTask{
|
||||
{ID: "1", Title: "Review RFC", Status: "completed"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
plan, err := readPlan(PlansRoot(), created.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
r := s.cmdPlanCheck(core.NewOptions(core.Option{Key: "_arg", Value: plan.Slug}))
|
||||
require.True(t, r.OK)
|
||||
|
||||
output, ok := r.Value.(PlanCheckOutput)
|
||||
require.True(t, ok)
|
||||
assert.True(t, output.Success)
|
||||
assert.True(t, output.Complete)
|
||||
assert.Empty(t, output.Pending)
|
||||
assert.Equal(t, plan.Slug, output.Plan.Slug)
|
||||
}
|
||||
|
||||
func TestCommandsPlan_CmdPlanCheck_Bad_MissingSlug(t *testing.T) {
|
||||
s := newTestPrep(t)
|
||||
|
||||
r := s.cmdPlanCheck(core.NewOptions())
|
||||
|
||||
assert.False(t, r.OK)
|
||||
require.Error(t, r.Value.(error))
|
||||
assert.Contains(t, r.Value.(error).Error(), "slug is required")
|
||||
}
|
||||
|
||||
func TestCommandsPlan_CmdPlanCheck_Ugly_IncompletePhase(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("CORE_WORKSPACE", dir)
|
||||
|
||||
s := newTestPrep(t)
|
||||
_, created, err := s.planCreate(context.Background(), nil, PlanCreateInput{
|
||||
Title: "Incomplete Plan",
|
||||
Description: "Leave one task pending",
|
||||
Phases: []Phase{
|
||||
{
|
||||
Number: 1,
|
||||
Name: "Setup",
|
||||
Tasks: []PlanTask{
|
||||
{ID: "1", Title: "Review RFC", Status: "completed"},
|
||||
{ID: "2", Title: "Patch code", Status: "pending"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
plan, err := readPlan(PlansRoot(), created.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
r := s.cmdPlanCheck(core.NewOptions(
|
||||
core.Option{Key: "slug", Value: plan.Slug},
|
||||
core.Option{Key: "phase", Value: 1},
|
||||
))
|
||||
|
||||
assert.False(t, r.OK)
|
||||
output, ok := r.Value.(PlanCheckOutput)
|
||||
require.True(t, ok)
|
||||
assert.False(t, output.Complete)
|
||||
assert.Equal(t, 1, output.Phase)
|
||||
assert.Equal(t, "Setup", output.PhaseName)
|
||||
assert.Equal(t, []string{"Patch code"}, output.Pending)
|
||||
}
|
||||
|
|
@ -1233,6 +1233,7 @@ 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, "plan/check")
|
||||
assert.Contains(t, cmds, "plan/archive")
|
||||
assert.Contains(t, cmds, "plan/delete")
|
||||
assert.Contains(t, cmds, "pr-manage")
|
||||
|
|
|
|||
|
|
@ -67,6 +67,16 @@ type PlanArchiveOutput struct {
|
|||
Archived string `json:"archived"`
|
||||
}
|
||||
|
||||
// out := agentic.PlanCheckOutput{Success: true, Complete: true}
|
||||
type PlanCheckOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Complete bool `json:"complete"`
|
||||
Plan PlanCompatibilityView `json:"plan"`
|
||||
Phase int `json:"phase,omitempty"`
|
||||
PhaseName string `json:"phase_name,omitempty"`
|
||||
Pending []string `json:"pending,omitempty"`
|
||||
}
|
||||
|
||||
// result := c.Action("plan.get").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "my-plan-abc123"}))
|
||||
func (s *PrepSubsystem) handlePlanGet(ctx context.Context, options core.Options) core.Result {
|
||||
return s.handlePlanRead(ctx, options)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue