From dd01f366f201742b363576aea1851fdc675bb522 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 11:05:37 +0000 Subject: [PATCH] feat(agentic): add plan checkpoint tool Co-Authored-By: Virgil --- pkg/mcp/agentic/plan.go | 83 +++++++++++++++++++++++++++++++++--- pkg/mcp/agentic/plan_test.go | 62 +++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 pkg/mcp/agentic/plan_test.go diff --git a/pkg/mcp/agentic/plan.go b/pkg/mcp/agentic/plan.go index db2cf8d..ef33b79 100644 --- a/pkg/mcp/agentic/plan.go +++ b/pkg/mcp/agentic/plan.go @@ -20,7 +20,7 @@ import ( type Plan struct { ID string `json:"id"` Title string `json:"title"` - Status string `json:"status"` // draft, ready, in_progress, needs_verification, verified, approved + Status string `json:"status"` // draft, ready, in_progress, needs_verification, verified, approved Repo string `json:"repo,omitempty"` Org string `json:"org,omitempty"` Objective string `json:"objective"` @@ -33,12 +33,20 @@ type Plan struct { // Phase represents a phase within an implementation plan. type Phase struct { - Number int `json:"number"` - Name string `json:"name"` - Status string `json:"status"` // pending, in_progress, done - Criteria []string `json:"criteria,omitempty"` - Tests int `json:"tests,omitempty"` - Notes string `json:"notes,omitempty"` + Number int `json:"number"` + Name string `json:"name"` + Status string `json:"status"` // pending, in_progress, done + Criteria []string `json:"criteria,omitempty"` + Tests int `json:"tests,omitempty"` + Notes string `json:"notes,omitempty"` + Checkpoints []Checkpoint `json:"checkpoints,omitempty"` +} + +// Checkpoint records phase progress or completion details. +type Checkpoint struct { + Notes string `json:"notes,omitempty"` + Done bool `json:"done,omitempty"` + CreatedAt time.Time `json:"created_at"` } // --- Input/Output types --- @@ -112,6 +120,20 @@ type PlanListOutput struct { Plans []Plan `json:"plans"` } +// PlanCheckpointInput is the input for agentic_plan_checkpoint. +type PlanCheckpointInput struct { + ID string `json:"id"` + Phase int `json:"phase"` + Notes string `json:"notes,omitempty"` + Done bool `json:"done,omitempty"` +} + +// PlanCheckpointOutput is the output for agentic_plan_checkpoint. +type PlanCheckpointOutput struct { + Success bool `json:"success"` + Plan Plan `json:"plan"` +} + // --- Registration --- func (s *PrepSubsystem) registerPlanTools(server *mcp.Server) { @@ -139,6 +161,11 @@ func (s *PrepSubsystem) registerPlanTools(server *mcp.Server) { Name: "agentic_plan_list", Description: "List implementation plans. Supports filtering by status (draft, ready, in_progress, etc.) and repo.", }, s.planList) + + mcp.AddTool(server, &mcp.Tool{ + Name: "agentic_plan_checkpoint", + Description: "Record a checkpoint for a plan phase and optionally mark the phase done.", + }, s.planCheckpoint) } // --- Handlers --- @@ -309,6 +336,48 @@ func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, inpu }, nil } +func (s *PrepSubsystem) planCheckpoint(_ context.Context, _ *mcp.CallToolRequest, input PlanCheckpointInput) (*mcp.CallToolResult, PlanCheckpointOutput, error) { + if input.ID == "" { + return nil, PlanCheckpointOutput{}, coreerr.E("planCheckpoint", "id is required", nil) + } + if input.Phase <= 0 { + return nil, PlanCheckpointOutput{}, coreerr.E("planCheckpoint", "phase must be greater than zero", nil) + } + if input.Notes == "" && !input.Done { + return nil, PlanCheckpointOutput{}, coreerr.E("planCheckpoint", "notes or done is required", nil) + } + + plan, err := readPlan(s.plansDir(), input.ID) + if err != nil { + return nil, PlanCheckpointOutput{}, err + } + + phaseIndex := input.Phase - 1 + if phaseIndex >= len(plan.Phases) { + return nil, PlanCheckpointOutput{}, coreerr.E("planCheckpoint", "phase not found", nil) + } + + phase := &plan.Phases[phaseIndex] + phase.Checkpoints = append(phase.Checkpoints, Checkpoint{ + Notes: input.Notes, + Done: input.Done, + CreatedAt: time.Now(), + }) + if input.Done { + phase.Status = "done" + } + + plan.UpdatedAt = time.Now() + if _, err := writePlan(s.plansDir(), plan); err != nil { + return nil, PlanCheckpointOutput{}, coreerr.E("planCheckpoint", "failed to write plan", err) + } + + return nil, PlanCheckpointOutput{ + Success: true, + Plan: *plan, + }, nil +} + // --- Helpers --- func (s *PrepSubsystem) plansDir() string { diff --git a/pkg/mcp/agentic/plan_test.go b/pkg/mcp/agentic/plan_test.go new file mode 100644 index 0000000..8e28e62 --- /dev/null +++ b/pkg/mcp/agentic/plan_test.go @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "context" + "testing" + "time" +) + +func TestPlanCheckpoint_Good_AppendsCheckpointAndMarksPhaseDone(t *testing.T) { + root := t.TempDir() + sub := &PrepSubsystem{codePath: root} + + plan := &Plan{ + ID: "plan-1", + Title: "Test plan", + Status: "in_progress", + Objective: "Verify checkpoints", + Phases: []Phase{ + { + Number: 1, + Name: "Phase 1", + Status: "in_progress", + }, + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if _, err := writePlan(sub.plansDir(), plan); err != nil { + t.Fatalf("writePlan failed: %v", err) + } + + _, out, err := sub.planCheckpoint(context.Background(), nil, PlanCheckpointInput{ + ID: plan.ID, + Phase: 1, + Notes: "Implementation verified", + Done: true, + }) + if err != nil { + t.Fatalf("planCheckpoint failed: %v", err) + } + if !out.Success { + t.Fatal("expected checkpoint output success") + } + if out.Plan.Phases[0].Status != "done" { + t.Fatalf("expected phase status done, got %q", out.Plan.Phases[0].Status) + } + if len(out.Plan.Phases[0].Checkpoints) != 1 { + t.Fatalf("expected 1 checkpoint, got %d", len(out.Plan.Phases[0].Checkpoints)) + } + if out.Plan.Phases[0].Checkpoints[0].Notes != "Implementation verified" { + t.Fatalf("unexpected checkpoint notes: %q", out.Plan.Phases[0].Checkpoints[0].Notes) + } + if !out.Plan.Phases[0].Checkpoints[0].Done { + t.Fatal("expected checkpoint to be marked done") + } + if out.Plan.Phases[0].Checkpoints[0].CreatedAt.IsZero() { + t.Fatal("expected checkpoint timestamp") + } +}