feat(agentic): add plan check command

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 14:12:15 +00:00
parent c55f4e4f5e
commit 460af585ed
4 changed files with 214 additions and 0 deletions

View file

@ -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
}

View 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)
}

View file

@ -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")

View file

@ -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)