From 00f37096fa067f44bc52cc22be944d76419ccb5d Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 15:28:47 +0000 Subject: [PATCH] feat(agentic): expose plan check action and tool Co-Authored-By: Virgil --- pkg/agentic/commands_plan.go | 17 ++++++++++++---- pkg/agentic/commands_plan_test.go | 34 +++++++++++++++++++++++++++++++ pkg/agentic/plan.go | 5 +++++ pkg/agentic/plan_compat.go | 31 ++++++++++++++++++++++++++++ pkg/agentic/prep.go | 1 + pkg/agentic/prep_test.go | 1 + 6 files changed, 85 insertions(+), 4 deletions(-) diff --git a/pkg/agentic/commands_plan.go b/pkg/agentic/commands_plan.go index 1f8858b..e360d02 100644 --- a/pkg/agentic/commands_plan.go +++ b/pkg/agentic/commands_plan.go @@ -181,14 +181,23 @@ func (s *PrepSubsystem) cmdPlanCheck(options core.Options) core.Result { 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 { + result := s.handlePlanCheck(ctx, core.NewOptions( + core.Option{Key: "slug", Value: slug}, + core.Option{Key: "phase", Value: optionIntValue(options, "phase", "phase_order")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdPlanCheck", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + check, ok := result.Value.(PlanCheckOutput) + if !ok { + err := core.E("agentic.cmdPlanCheck", "invalid plan check output", 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) diff --git a/pkg/agentic/commands_plan_test.go b/pkg/agentic/commands_plan_test.go index 74801f9..d9d8a73 100644 --- a/pkg/agentic/commands_plan_test.go +++ b/pkg/agentic/commands_plan_test.go @@ -91,3 +91,37 @@ func TestCommandsPlan_CmdPlanCheck_Ugly_IncompletePhase(t *testing.T) { assert.Equal(t, "Setup", output.PhaseName) assert.Equal(t, []string{"Patch code"}, output.Pending) } + +func TestCommandsPlan_HandlePlanCheck_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: "Action Check Plan", + Description: "Confirm the plan check action 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.handlePlanCheck(context.Background(), core.NewOptions( + core.Option{Key: "slug", 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.Equal(t, plan.Slug, output.Plan.Slug) +} diff --git a/pkg/agentic/plan.go b/pkg/agentic/plan.go index d11a821..785dc7e 100644 --- a/pkg/agentic/plan.go +++ b/pkg/agentic/plan.go @@ -268,6 +268,11 @@ func (s *PrepSubsystem) registerPlanTools(server *mcp.Server) { Description: "List plans using the compatibility surface with slug and progress summaries.", }, s.planListCompat) + mcp.AddTool(server, &mcp.Tool{ + Name: "plan_check", + Description: "Check whether a plan or phase is complete using the compatibility surface.", + }, s.planCheck) + mcp.AddTool(server, &mcp.Tool{ Name: "plan_update", Description: "Update a plan using the legacy plain-name MCP alias.", diff --git a/pkg/agentic/plan_compat.go b/pkg/agentic/plan_compat.go index 2d1a066..bd77457 100644 --- a/pkg/agentic/plan_compat.go +++ b/pkg/agentic/plan_compat.go @@ -68,6 +68,12 @@ type PlanArchiveOutput struct { Archived string `json:"archived"` } +// input := agentic.PlanCheckInput{Slug: "my-plan-abc123", Phase: 1} +type PlanCheckInput struct { + Slug string `json:"slug"` + Phase int `json:"phase,omitempty"` +} + // out := agentic.PlanCheckOutput{Success: true, Complete: true} type PlanCheckOutput struct { Success bool `json:"success"` @@ -108,6 +114,18 @@ func (s *PrepSubsystem) handlePlanUpdateStatus(ctx context.Context, options core return core.Result{Value: output, OK: true} } +// result := c.Action("plan.check").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "my-plan-abc123"})) +func (s *PrepSubsystem) handlePlanCheck(ctx context.Context, options core.Options) core.Result { + _, output, err := s.planCheck(ctx, nil, PlanCheckInput{ + Slug: optionStringValue(options, "slug", "_arg"), + Phase: optionIntValue(options, "phase", "phase_order"), + }) + if err != nil { + return core.Result{Value: err, OK: false} + } + return core.Result{Value: output, OK: true} +} + func (s *PrepSubsystem) planCreateCompat(ctx context.Context, _ *mcp.CallToolRequest, input PlanCreateInput) (*mcp.CallToolResult, PlanCompatibilityCreateOutput, error) { _, created, err := s.planCreate(ctx, nil, input) if err != nil { @@ -186,6 +204,19 @@ func (s *PrepSubsystem) planUpdateStatusCompat(ctx context.Context, _ *mcp.CallT }, nil } +func (s *PrepSubsystem) planCheck(ctx context.Context, _ *mcp.CallToolRequest, input PlanCheckInput) (*mcp.CallToolResult, PlanCheckOutput, error) { + if input.Slug == "" { + return nil, PlanCheckOutput{}, core.E("planCheck", "slug is required", nil) + } + + _, output, err := s.planGetCompat(ctx, nil, PlanReadInput{Slug: input.Slug}) + if err != nil { + return nil, PlanCheckOutput{}, err + } + + return nil, planCheckOutput(output.Plan, input.Phase), nil +} + func (s *PrepSubsystem) planArchiveCompat(ctx context.Context, _ *mcp.CallToolRequest, input PlanDeleteInput) (*mcp.CallToolResult, PlanArchiveOutput, error) { plan, err := archivePlanResult(input, "slug is required", "planArchiveCompat") if err != nil { diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 7ed5f6a..13b031f 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -184,6 +184,7 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result { c.Action("plan.read", s.handlePlanRead).Description = "Read an implementation plan by ID" c.Action("plan.update", s.handlePlanUpdate).Description = "Update plan status, phases, notes, or agent assignment" c.Action("plan.update_status", s.handlePlanUpdateStatus).Description = "Update an implementation plan lifecycle status by slug" + c.Action("plan.check", s.handlePlanCheck).Description = "Check whether a plan or phase is complete" c.Action("plan.archive", s.handlePlanArchive).Description = "Archive an implementation plan by slug" c.Action("plan.delete", s.handlePlanDelete).Description = "Archive an implementation plan by ID" c.Action("plan.list", s.handlePlanList).Description = "List implementation plans with optional filters" diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index 1390a35..bc281d8 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -445,6 +445,7 @@ func TestPrep_OnStartup_Good_RegistersPlanActions(t *testing.T) { assert.True(t, c.Action("plan.read").Exists()) assert.True(t, c.Action("plan.update").Exists()) assert.True(t, c.Action("plan.update_status").Exists()) + assert.True(t, c.Action("plan.check").Exists()) assert.True(t, c.Action("plan.archive").Exists()) assert.True(t, c.Action("plan.delete").Exists()) assert.True(t, c.Action("plan.list").Exists())