// SPDX-License-Identifier: EUPL-1.2 package agentic import ( "context" "crypto/rand" "encoding/hex" "strconv" "sync/atomic" "time" core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) // plan := &Plan{ID: "id-1-a3f2b1", Title: "Migrate Core", Status: "draft", Objective: "Replace raw process calls with Core.Process()"} // r := writePlanResult(PlansRoot(), plan) type Plan struct { ID string `json:"id"` WorkspaceID int `json:"workspace_id,omitempty"` Slug string `json:"slug,omitempty"` Title string `json:"title"` Status string `json:"status"` Repo string `json:"repo,omitempty"` Org string `json:"org,omitempty"` Objective string `json:"objective"` Description string `json:"description,omitempty"` AgentType string `json:"agent_type,omitempty"` Context map[string]any `json:"context,omitempty"` TemplateVersionID int `json:"template_version_id,omitempty"` TemplateVersion PlanTemplateVersion `json:"template_version,omitempty"` Phases []Phase `json:"phases,omitempty"` Notes string `json:"notes,omitempty"` Agent string `json:"agent,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` ArchivedAt time.Time `json:"archived_at,omitempty"` } // plan := agentic.AgentPlan{Slug: "ax-follow-up", Title: "AX follow-up"} type AgentPlan = Plan // phase := agentic.Phase{Number: 1, Name: "Migrate strings", Status: "in_progress"} type Phase struct { AgentPlanID int `json:"agent_plan_id,omitempty"` Number int `json:"number"` Name string `json:"name"` Description string `json:"description,omitempty"` Status string `json:"status"` Criteria []string `json:"criteria,omitempty"` CompletionCriteria []string `json:"completion_criteria,omitempty"` Dependencies []string `json:"dependencies,omitempty"` Tasks []PlanTask `json:"tasks,omitempty"` Checkpoints []PhaseCheckpoint `json:"checkpoints,omitempty"` Tests int `json:"tests,omitempty"` Notes string `json:"notes,omitempty"` } // task := agentic.PlanTask{ID: "1", Title: "Review imports", Status: "pending", File: "pkg/agentic/plan.go", Line: 46} type PlanTask struct { ID string `json:"id,omitempty"` Title string `json:"title"` Description string `json:"description,omitempty"` Priority string `json:"priority,omitempty"` Category string `json:"category,omitempty"` Status string `json:"status,omitempty"` Notes string `json:"notes,omitempty"` File string `json:"file,omitempty"` Line int `json:"line,omitempty"` FileRef string `json:"file_ref,omitempty"` LineRef int `json:"line_ref,omitempty"` } // task := agentic.Task{ID: "1", Title: "Review imports"} type Task = PlanTask // checkpoint := agentic.PhaseCheckpoint{Note: "Build passes", CreatedAt: "2026-03-31T00:00:00Z"} type PhaseCheckpoint struct { Note string `json:"note"` Context map[string]any `json:"context,omitempty"` CreatedAt string `json:"created_at,omitempty"` } type PlanCreateInput struct { Title string `json:"title"` Slug string `json:"slug,omitempty"` Objective string `json:"objective,omitempty"` Description string `json:"description,omitempty"` Context map[string]any `json:"context,omitempty"` AgentType string `json:"agent_type,omitempty"` TemplateVersion PlanTemplateVersion `json:"template_version,omitempty"` Repo string `json:"repo,omitempty"` Org string `json:"org,omitempty"` Phases []Phase `json:"phases,omitempty"` Notes string `json:"notes,omitempty"` } type PlanCreateOutput struct { Success bool `json:"success"` ID string `json:"id"` Path string `json:"path"` } type PlanReadInput struct { ID string `json:"id,omitempty"` Slug string `json:"slug,omitempty"` } type PlanReadOutput struct { Success bool `json:"success"` Plan Plan `json:"plan"` } type PlanUpdateInput struct { ID string `json:"id,omitempty"` Slug string `json:"slug,omitempty"` Status string `json:"status,omitempty"` Title string `json:"title,omitempty"` Objective string `json:"objective,omitempty"` Description string `json:"description,omitempty"` Context map[string]any `json:"context,omitempty"` Phases []Phase `json:"phases,omitempty"` Notes string `json:"notes,omitempty"` Agent string `json:"agent,omitempty"` AgentType string `json:"agent_type,omitempty"` } type PlanUpdateOutput struct { Success bool `json:"success"` Plan Plan `json:"plan"` } type PlanDeleteInput struct { ID string `json:"id,omitempty"` Slug string `json:"slug,omitempty"` Reason string `json:"reason,omitempty"` } type PlanDeleteOutput struct { Success bool `json:"success"` Deleted string `json:"deleted"` } type PlanListInput struct { Status string `json:"status,omitempty"` Repo string `json:"repo,omitempty"` Limit int `json:"limit,omitempty"` } type PlanListOutput struct { Success bool `json:"success"` Count int `json:"count"` Plans []Plan `json:"plans"` } const planListDefaultLimit = 20 var planIDCounter atomic.Uint64 // result := c.Action("plan.create").Run(ctx, core.NewOptions( // // core.Option{Key: "title", Value: "AX RFC follow-up"}, // core.Option{Key: "objective", Value: "Register plan actions"}, // // )) func (s *PrepSubsystem) handlePlanCreate(ctx context.Context, options core.Options) core.Result { _, output, err := s.planCreate(ctx, nil, PlanCreateInput{ Title: optionStringValue(options, "title"), Slug: optionStringValue(options, "slug"), Objective: optionStringValue(options, "objective"), Description: optionStringValue(options, "description"), Context: optionAnyMapValue(options, "context"), AgentType: optionStringValue(options, "agent_type", "agent"), Repo: optionStringValue(options, "repo"), Org: optionStringValue(options, "org"), Phases: planPhasesValue(options, "phases"), Notes: optionStringValue(options, "notes"), }) if err != nil { return core.Result{Value: err, OK: false} } return core.Result{Value: output, OK: true} } // result := c.Action("plan.read").Run(ctx, core.NewOptions(core.Option{Key: "id", Value: "id-42-a3f2b1"})) func (s *PrepSubsystem) handlePlanRead(ctx context.Context, options core.Options) core.Result { _, output, err := s.planRead(ctx, nil, PlanReadInput{ ID: optionStringValue(options, "id", "_arg"), Slug: optionStringValue(options, "slug"), }) if err != nil { return core.Result{Value: err, OK: false} } return core.Result{Value: output, OK: true} } // result := c.Action("plan.update").Run(ctx, core.NewOptions( // // core.Option{Key: "id", Value: "id-42-a3f2b1"}, // core.Option{Key: "status", Value: "ready"}, // // )) func (s *PrepSubsystem) handlePlanUpdate(ctx context.Context, options core.Options) core.Result { _, output, err := s.planUpdate(ctx, nil, PlanUpdateInput{ ID: optionStringValue(options, "id", "_arg"), Slug: optionStringValue(options, "slug"), Status: optionStringValue(options, "status"), Title: optionStringValue(options, "title"), Objective: optionStringValue(options, "objective"), Description: optionStringValue(options, "description"), Context: optionAnyMapValue(options, "context"), Phases: planPhasesValue(options, "phases"), Notes: optionStringValue(options, "notes"), Agent: optionStringValue(options, "agent"), AgentType: optionStringValue(options, "agent_type", "agent-type"), }) if err != nil { return core.Result{Value: err, OK: false} } return core.Result{Value: output, OK: true} } // result := c.Action("plan.delete").Run(ctx, core.NewOptions(core.Option{Key: "id", Value: "id-42-a3f2b1"})) func (s *PrepSubsystem) handlePlanDelete(ctx context.Context, options core.Options) core.Result { _, output, err := s.planDelete(ctx, nil, PlanDeleteInput{ ID: optionStringValue(options, "id", "_arg"), Slug: optionStringValue(options, "slug"), Reason: optionStringValue(options, "reason"), }) if err != nil { return core.Result{Value: err, OK: false} } return core.Result{Value: output, OK: true} } // result := c.Action("plan.list").Run(ctx, core.NewOptions(core.Option{Key: "repo", Value: "go-io"})) func (s *PrepSubsystem) handlePlanList(ctx context.Context, options core.Options) core.Result { _, output, err := s.planList(ctx, nil, PlanListInput{ Status: optionStringValue(options, "status"), Repo: optionStringValue(options, "repo"), Limit: optionIntValue(options, "limit"), }) if err != nil { return core.Result{Value: err, OK: false} } return core.Result{Value: output, OK: true} } func (s *PrepSubsystem) registerPlanTools(server *mcp.Server) { mcp.AddTool(server, &mcp.Tool{ Name: "agentic_plan_create", Description: "Create an implementation plan. Plans track phased work with acceptance criteria, status lifecycle (draft → ready → in_progress → needs_verification → verified → approved), and per-phase progress.", }, s.planCreate) mcp.AddTool(server, &mcp.Tool{ Name: "agentic_plan_read", Description: "Read an implementation plan by ID. Returns the full plan with all phases, criteria, and status.", }, s.planRead) mcp.AddTool(server, &mcp.Tool{ Name: "agentic_plan_get", Description: "Read an implementation plan by slug with progress details and full phases.", }, s.planGetCompat) mcp.AddTool(server, &mcp.Tool{ Name: "agentic_plan_update", Description: "Update an implementation plan. Supports partial updates — only provided fields are changed. Use this to advance status, update phases, or add notes.", }, s.planUpdate) mcp.AddTool(server, &mcp.Tool{ Name: "agentic_plan_delete", Description: "Delete an implementation plan by ID. Permanently removes the plan file.", }, s.planDelete) mcp.AddTool(server, &mcp.Tool{ 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: "plan_create", Description: "Create a plan using the slug-based compatibility surface described by the platform RFC.", }, s.planCreateCompat) mcp.AddTool(server, &mcp.Tool{ Name: "plan_read", Description: "Read a plan using the legacy plain-name MCP alias.", }, s.planRead) mcp.AddTool(server, &mcp.Tool{ Name: "plan_get", Description: "Read a plan by slug with progress details and full phases.", }, s.planGetCompat) mcp.AddTool(server, &mcp.Tool{ Name: "plan_list", 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: "agentic_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.", }, s.planUpdate) mcp.AddTool(server, &mcp.Tool{ Name: "plan_update_status", Description: "Update a plan lifecycle status by slug.", }, s.planUpdateStatusCompat) mcp.AddTool(server, &mcp.Tool{ Name: "agentic_plan_update_status", Description: "Update a plan lifecycle status by slug.", }, s.planUpdateStatusCompat) mcp.AddTool(server, &mcp.Tool{ Name: "plan_delete", Description: "Delete a plan using the legacy plain-name MCP alias.", }, s.planDelete) mcp.AddTool(server, &mcp.Tool{ Name: "plan_archive", Description: "Archive a plan by slug without deleting the local record.", }, s.planArchiveCompat) mcp.AddTool(server, &mcp.Tool{ Name: "agentic_plan_archive", Description: "Archive a plan by slug without deleting the local record.", }, s.planArchiveCompat) mcp.AddTool(server, &mcp.Tool{ Name: "plan_from_issue", Description: "Create an implementation plan from a tracked issue slug or ID.", }, s.planFromIssue) mcp.AddTool(server, &mcp.Tool{ Name: "agentic_plan_from_issue", Description: "Create an implementation plan from a tracked issue slug or ID.", }, s.planFromIssue) } func (s *PrepSubsystem) planCreate(_ context.Context, _ *mcp.CallToolRequest, input PlanCreateInput) (*mcp.CallToolResult, PlanCreateOutput, error) { if input.Title == "" { return nil, PlanCreateOutput{}, core.E("planCreate", "title is required", nil) } description := input.Description if description == "" { description = input.Objective } objective := input.Objective if objective == "" { objective = description } if objective == "" { return nil, PlanCreateOutput{}, core.E("planCreate", "objective is required", nil) } id := planID() plan := Plan{ ID: id, Slug: planSlugValue(input.Slug, input.Title, id), Title: input.Title, Status: "draft", Repo: input.Repo, Org: input.Org, Objective: objective, Description: description, AgentType: core.Trim(input.AgentType), Context: input.Context, TemplateVersion: input.TemplateVersion, Phases: input.Phases, Notes: input.Notes, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if plan.AgentType == "" { plan.AgentType = core.Trim(plan.Agent) } plan = normalisePlan(plan) writeResult := writePlanResult(PlansRoot(), &plan) if !writeResult.OK { err, _ := writeResult.Value.(error) if err == nil { err = core.E("planCreate", "failed to write plan", nil) } return nil, PlanCreateOutput{}, err } path, ok := writeResult.Value.(string) if !ok { return nil, PlanCreateOutput{}, core.E("planCreate", "invalid plan write result", nil) } return nil, PlanCreateOutput{ Success: true, ID: id, Path: path, }, nil } func (s *PrepSubsystem) planRead(_ context.Context, _ *mcp.CallToolRequest, input PlanReadInput) (*mcp.CallToolResult, PlanReadOutput, error) { ref := planReference(input.ID, input.Slug) if ref == "" { return nil, PlanReadOutput{}, core.E("planRead", "id is required", nil) } planResult := readPlanResult(PlansRoot(), ref) if !planResult.OK { err, _ := planResult.Value.(error) if err == nil { err = core.E("planRead", "failed to read plan", nil) } return nil, PlanReadOutput{}, err } plan, ok := planResult.Value.(*Plan) if !ok || plan == nil { return nil, PlanReadOutput{}, core.E("planRead", "invalid plan payload", nil) } return nil, PlanReadOutput{ Success: true, Plan: *plan, }, nil } func (s *PrepSubsystem) planUpdate(_ context.Context, _ *mcp.CallToolRequest, input PlanUpdateInput) (*mcp.CallToolResult, PlanUpdateOutput, error) { ref := planReference(input.ID, input.Slug) if ref == "" { return nil, PlanUpdateOutput{}, core.E("planUpdate", "id is required", nil) } planResult := readPlanResult(PlansRoot(), ref) if !planResult.OK { err, _ := planResult.Value.(error) if err == nil { err = core.E("planUpdate", "failed to read plan", nil) } return nil, PlanUpdateOutput{}, err } plan, ok := planResult.Value.(*Plan) if !ok || plan == nil { return nil, PlanUpdateOutput{}, core.E("planUpdate", "invalid plan payload", nil) } if input.Status != "" { if !validPlanStatus(input.Status) { return nil, PlanUpdateOutput{}, core.E("planUpdate", core.Concat("invalid status: ", input.Status, " (valid: draft, ready, in_progress, needs_verification, verified, approved)"), nil) } plan.Status = input.Status } if input.Title != "" { plan.Title = input.Title } if input.Slug != "" { plan.Slug = planSlugValue(input.Slug, plan.Title, plan.ID) } if input.Objective != "" { plan.Objective = input.Objective if plan.Description == "" { plan.Description = input.Objective } } if input.Description != "" { plan.Description = input.Description if plan.Objective == "" || input.Objective == "" { plan.Objective = input.Description } } if input.Context != nil { plan.Context = input.Context } if input.Phases != nil { plan.Phases = input.Phases } if input.Notes != "" { plan.Notes = input.Notes } if input.Agent != "" { plan.Agent = input.Agent } if input.AgentType != "" { plan.AgentType = input.AgentType } *plan = normalisePlan(*plan) plan.UpdatedAt = time.Now() writeResult := writePlanResult(PlansRoot(), plan) if !writeResult.OK { err, _ := writeResult.Value.(error) if err == nil { err = core.E("planUpdate", "failed to write plan", nil) } return nil, PlanUpdateOutput{}, err } return nil, PlanUpdateOutput{ Success: true, Plan: *plan, }, nil } func (s *PrepSubsystem) planDelete(_ context.Context, _ *mcp.CallToolRequest, input PlanDeleteInput) (*mcp.CallToolResult, PlanDeleteOutput, error) { plan, err := deletePlanResult(input, "id is required", "planDelete") if err != nil { return nil, PlanDeleteOutput{}, err } return nil, PlanDeleteOutput{ Success: true, Deleted: plan.ID, }, nil } func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, input PlanListInput) (*mcp.CallToolResult, PlanListOutput, error) { dir := PlansRoot() if r := fs.EnsureDir(dir); !r.OK { err, _ := r.Value.(error) return nil, PlanListOutput{}, core.E("planList", "failed to access plans directory", err) } limit := input.Limit if limit <= 0 { limit = planListDefaultLimit } jsonFiles := core.PathGlob(core.JoinPath(dir, "*.json")) var plans []Plan for _, f := range jsonFiles { id := core.TrimSuffix(core.PathBase(f), ".json") planResult := readPlanResult(dir, id) if !planResult.OK { continue } plan, ok := planResult.Value.(*Plan) if !ok || plan == nil { continue } if input.Status != "" && plan.Status != input.Status { continue } if input.Repo != "" && plan.Repo != input.Repo { continue } plans = append(plans, *plan) if len(plans) >= limit { break } } return nil, PlanListOutput{ Success: true, Count: len(plans), Plans: plans, }, nil } func planPath(dir, id string) string { return core.JoinPath(dir, core.Concat(pathKey(id), ".json")) } func planPhasesValue(options core.Options, keys ...string) []Phase { for _, key := range keys { result := options.Get(key) if !result.OK { continue } phases := phaseSliceValue(result.Value) if len(phases) > 0 { return phases } } return nil } func phaseSliceValue(value any) []Phase { switch typed := value.(type) { case []Phase: return typed case []any: phases := make([]Phase, 0, len(typed)) for _, item := range typed { phase, ok := phaseValue(item) if ok { phases = append(phases, phase) } } return phases case string: trimmed := core.Trim(typed) if trimmed == "" { return nil } if core.HasPrefix(trimmed, "[") { var phases []Phase if result := core.JSONUnmarshalString(trimmed, &phases); result.OK { return phases } if values := anyMapSliceValue(trimmed); len(values) > 0 { return phaseSliceValue(values) } var generic []any if result := core.JSONUnmarshalString(trimmed, &generic); result.OK { return phaseSliceValue(generic) } } case []map[string]any: phases := make([]Phase, 0, len(typed)) for _, item := range typed { phase, ok := phaseValue(item) if ok { phases = append(phases, phase) } } return phases } if phase, ok := phaseValue(value); ok { return []Phase{phase} } return nil } func phaseValue(value any) (Phase, bool) { switch typed := value.(type) { case Phase: return typed, true case map[string]any: criteria := stringSliceValue(typed["criteria"]) completionCriteria := phaseCriteriaValue(typed["completion_criteria"], typed["completion-criteria"]) if len(criteria) == 0 { criteria = completionCriteria } if len(completionCriteria) == 0 { completionCriteria = criteria } return Phase{ Number: intValue(typed["number"]), Name: stringValue(typed["name"]), Description: stringValue(typed["description"]), Status: stringValue(typed["status"]), Criteria: criteria, CompletionCriteria: completionCriteria, Dependencies: phaseDependenciesValue(typed["dependencies"]), Tasks: planTaskSliceValue(typed["tasks"]), Checkpoints: phaseCheckpointSliceValue(typed["checkpoints"]), Tests: intValue(typed["tests"]), Notes: stringValue(typed["notes"]), }, true case map[string]string: return phaseValue(anyMapValue(typed)) case string: trimmed := core.Trim(typed) if trimmed == "" || !core.HasPrefix(trimmed, "{") { return Phase{}, false } if values := anyMapValue(trimmed); len(values) > 0 { return phaseValue(values) } } return Phase{}, false } func phaseDependenciesValue(value any) []string { switch typed := value.(type) { case []string: return cleanStrings(typed) case []any: dependencies := make([]string, 0, len(typed)) for _, item := range typed { text, ok := item.(string) if !ok { return nil } if text = core.Trim(text); text != "" { dependencies = append(dependencies, text) } } return dependencies case string: trimmed := core.Trim(typed) if trimmed == "" { return nil } if core.HasPrefix(trimmed, "[") { var dependencies []string if result := core.JSONUnmarshalString(trimmed, &dependencies); result.OK { return cleanStrings(dependencies) } return nil } return cleanStrings(core.Split(trimmed, ",")) default: if text := stringValue(value); text != "" { return []string{text} } } return nil } func phaseCriteriaValue(values ...any) []string { for _, value := range values { criteria := stringSliceValue(value) if len(criteria) > 0 { return criteria } } return nil } func planID() string { counter := planIDCounter.Add(1) suffix := planRandomHex() return core.Concat("id-", strconv.FormatUint(counter, 10), "-", suffix) } func planRandomHex() string { bytes := make([]byte, 3) if _, err := rand.Read(bytes); err != nil { return "000000" } return hex.EncodeToString(bytes) } func planTaskSliceValue(value any) []PlanTask { switch typed := value.(type) { case []PlanTask: return typed case []string: tasks := make([]PlanTask, 0, len(typed)) for _, title := range cleanStrings(typed) { tasks = append(tasks, PlanTask{Title: title}) } return tasks case []any: tasks := make([]PlanTask, 0, len(typed)) for _, item := range typed { if task, ok := planTaskValue(item); ok { tasks = append(tasks, task) } } return tasks case string: trimmed := core.Trim(typed) if trimmed == "" { return nil } if core.HasPrefix(trimmed, "[") { var tasks []PlanTask if result := core.JSONUnmarshalString(trimmed, &tasks); result.OK { return tasks } var generic []any if result := core.JSONUnmarshalString(trimmed, &generic); result.OK { return planTaskSliceValue(generic) } var titles []string if result := core.JSONUnmarshalString(trimmed, &titles); result.OK { return planTaskSliceValue(titles) } } case []map[string]any: tasks := make([]PlanTask, 0, len(typed)) for _, item := range typed { if task, ok := planTaskValue(item); ok { tasks = append(tasks, task) } } return tasks } if task, ok := planTaskValue(value); ok { return []PlanTask{task} } return nil } func planTaskValue(value any) (PlanTask, bool) { switch typed := value.(type) { case PlanTask: return typed, true case map[string]any: title := stringValue(typed["title"]) if title == "" { title = stringValue(typed["name"]) } file := stringValue(typed["file"]) if file == "" { file = stringValue(typed["file_ref"]) } line := intValue(typed["line"]) if line == 0 { line = intValue(typed["line_ref"]) } return PlanTask{ ID: stringValue(typed["id"]), Title: title, Description: stringValue(typed["description"]), Priority: stringValue(typed["priority"]), Category: stringValue(typed["category"]), Status: stringValue(typed["status"]), Notes: stringValue(typed["notes"]), File: file, Line: line, FileRef: file, LineRef: line, }, title != "" case map[string]string: return planTaskValue(anyMapValue(typed)) case string: title := core.Trim(typed) if title == "" { return PlanTask{}, false } if core.HasPrefix(title, "{") { if values := anyMapValue(title); len(values) > 0 { return planTaskValue(values) } } return PlanTask{Title: title}, true } return PlanTask{}, false } func phaseCheckpointSliceValue(value any) []PhaseCheckpoint { switch typed := value.(type) { case []PhaseCheckpoint: return typed case []any: checkpoints := make([]PhaseCheckpoint, 0, len(typed)) for _, item := range typed { if checkpoint, ok := phaseCheckpointValue(item); ok { checkpoints = append(checkpoints, checkpoint) } } return checkpoints case string: trimmed := core.Trim(typed) if trimmed == "" { return nil } if core.HasPrefix(trimmed, "[") { var checkpoints []PhaseCheckpoint if result := core.JSONUnmarshalString(trimmed, &checkpoints); result.OK { return checkpoints } var generic []any if result := core.JSONUnmarshalString(trimmed, &generic); result.OK { return phaseCheckpointSliceValue(generic) } } case []map[string]any: checkpoints := make([]PhaseCheckpoint, 0, len(typed)) for _, item := range typed { if checkpoint, ok := phaseCheckpointValue(item); ok { checkpoints = append(checkpoints, checkpoint) } } return checkpoints } if checkpoint, ok := phaseCheckpointValue(value); ok { return []PhaseCheckpoint{checkpoint} } return nil } func phaseCheckpointValue(value any) (PhaseCheckpoint, bool) { switch typed := value.(type) { case PhaseCheckpoint: return typed, typed.Note != "" case map[string]any: note := stringValue(typed["note"]) return PhaseCheckpoint{ Note: note, Context: anyMapValue(typed["context"]), CreatedAt: stringValue(typed["created_at"]), }, note != "" case map[string]string: return phaseCheckpointValue(anyMapValue(typed)) case string: note := core.Trim(typed) if note == "" { return PhaseCheckpoint{}, false } if core.HasPrefix(note, "{") { if values := anyMapValue(note); len(values) > 0 { return phaseCheckpointValue(values) } } return PhaseCheckpoint{Note: note}, true } return PhaseCheckpoint{}, false } func phaseCriteriaList(phase Phase) []string { criteria := cleanStrings(phase.Criteria) completionCriteria := cleanStrings(phase.CompletionCriteria) if len(criteria) == 0 { return completionCriteria } if len(completionCriteria) == 0 { return criteria } merged := make([]string, 0, len(criteria)+len(completionCriteria)) seen := map[string]bool{} for _, value := range criteria { if seen[value] { continue } seen[value] = true merged = append(merged, value) } for _, value := range completionCriteria { if seen[value] { continue } seen[value] = true merged = append(merged, value) } return merged } // result := readPlanResult(PlansRoot(), "plan-id") // if result.OK { plan := result.Value.(*Plan) } func readPlanResult(dir, id string) core.Result { path := planPath(dir, id) r := fs.Read(path) if r.OK { return planFromReadResult(r, id) } if found := findPlanBySlugResult(dir, id); found.OK { return found } err, _ := r.Value.(error) if err == nil { return core.Result{Value: core.E("readPlan", core.Concat("plan not found: ", id), nil), OK: false} } return core.Result{Value: core.E("readPlan", core.Concat("plan not found: ", id), err), OK: false} } // plan, err := readPlan(PlansRoot(), "plan-id") func readPlan(dir, id string) (*Plan, error) { r := readPlanResult(dir, id) if !r.OK { err, _ := r.Value.(error) if err == nil { return nil, core.E("readPlan", "failed to read plan", nil) } return nil, err } plan, ok := r.Value.(*Plan) if !ok || plan == nil { return nil, core.E("readPlan", "invalid plan payload", nil) } return plan, nil } // result := writePlanResult(PlansRoot(), plan) // if result.OK { path := result.Value.(string) } func writePlanResult(dir string, plan *Plan) core.Result { if plan == nil { return core.Result{Value: core.E("writePlan", "plan is required", nil), OK: false} } normalised := normalisePlan(*plan) plan = &normalised if r := fs.EnsureDir(dir); !r.OK { err, _ := r.Value.(error) if err == nil { return core.Result{Value: core.E("writePlan", "failed to create plans directory", nil), OK: false} } return core.Result{Value: core.E("writePlan", "failed to create plans directory", err), OK: false} } path := planPath(dir, plan.ID) if r := fs.WriteAtomic(path, core.JSONMarshalString(plan)); !r.OK { err, _ := r.Value.(error) if err == nil { return core.Result{Value: core.E("writePlan", "failed to write plan", nil), OK: false} } return core.Result{Value: core.E("writePlan", "failed to write plan", err), OK: false} } return core.Result{Value: path, OK: true} } // path, err := writePlan(PlansRoot(), plan) func writePlan(dir string, plan *Plan) (string, error) { r := writePlanResult(dir, plan) if !r.OK { err, _ := r.Value.(error) if err == nil { return "", core.E("writePlan", "failed to write plan", nil) } return "", err } path, ok := r.Value.(string) if !ok { return "", core.E("writePlan", "invalid plan write result", nil) } return path, nil } func validPlanStatus(status string) bool { switch status { case "draft", "ready", "in_progress", "needs_verification", "verified", "approved": return true } return false } func normalisePlan(plan Plan) Plan { if plan.Slug == "" { plan.Slug = planSlugValue("", plan.Title, plan.ID) } if plan.Description == "" { plan.Description = plan.Objective } if plan.Objective == "" { plan.Objective = plan.Description } if plan.AgentType == "" { plan.AgentType = plan.Agent } if plan.Agent == "" { plan.Agent = plan.AgentType } for i := range plan.Phases { plan.Phases[i] = normalisePhase(plan.Phases[i], i+1) } return plan } func normalisePhase(phase Phase, number int) Phase { if phase.Number == 0 { phase.Number = number } if phase.Status == "" { phase.Status = "pending" } criteria := phaseCriteriaList(phase) phase.Criteria = criteria phase.CompletionCriteria = criteria for i := range phase.Tasks { phase.Tasks[i] = normalisePlanTask(phase.Tasks[i], i+1) } for i := range phase.Checkpoints { if phase.Checkpoints[i].CreatedAt == "" { phase.Checkpoints[i].CreatedAt = time.Now().Format(time.RFC3339) } } return phase } func normalisePlanTask(task PlanTask, index int) PlanTask { if task.ID == "" { task.ID = core.Sprint(index) } if task.Status == "" { task.Status = "pending" } if task.Title == "" { task.Title = task.Description } task.Priority = core.Trim(task.Priority) task.Category = core.Trim(task.Category) if task.File == "" { task.File = task.FileRef } if task.FileRef == "" { task.FileRef = task.File } if task.Line == 0 { task.Line = task.LineRef } if task.LineRef == 0 { task.LineRef = task.Line } return task } func planReference(id, slug string) string { if id != "" { return id } return slug } func planFromReadResult(result core.Result, ref string) core.Result { var plan Plan if ur := core.JSONUnmarshalString(result.Value.(string), &plan); !ur.OK { err, _ := ur.Value.(error) if err == nil { return core.Result{Value: core.E("readPlan", core.Concat("failed to parse plan ", ref), nil), OK: false} } return core.Result{Value: core.E("readPlan", core.Concat("failed to parse plan ", ref), err), OK: false} } normalised := normalisePlan(plan) return core.Result{Value: &normalised, OK: true} } func findPlanBySlugResult(dir, slug string) core.Result { ref := core.Trim(slug) if ref == "" { return core.Result{Value: core.E("readPlan", "plan not found: invalid", nil), OK: false} } for _, path := range core.PathGlob(core.JoinPath(dir, "*.json")) { result := fs.Read(path) if !result.OK { continue } planResult := planFromReadResult(result, ref) if !planResult.OK { continue } plan, ok := planResult.Value.(*Plan) if !ok || plan == nil { continue } if plan.Slug == ref || plan.ID == ref { return core.Result{Value: plan, OK: true} } } return core.Result{Value: core.E("readPlan", core.Concat("plan not found: ", ref), nil), OK: false} } func planSlugValue(input, title, id string) string { slug := cleanPlanSlug(input) if slug != "" { return slug } base := cleanPlanSlug(title) if base == "" { base = "plan" } suffix := planSlugSuffix(id) if suffix == "" { return base } return core.Concat(base, "-", suffix) } func cleanPlanSlug(value string) string { slug := core.Lower(core.Trim(value)) if slug == "" { return "" } for _, old := range []string{"/", "\\", "_", ".", ":", ";", ",", " ", "\t", "\n", "\r"} { slug = core.Replace(slug, old, "-") } for core.Contains(slug, "--") { slug = core.Replace(slug, "--", "-") } for core.HasPrefix(slug, "-") { slug = slug[1:] } for core.HasSuffix(slug, "-") { slug = slug[:len(slug)-1] } if slug == "" || slug == "invalid" { return "" } return slug } func planSlugSuffix(id string) string { parts := core.Split(id, "-") if len(parts) == 0 { return "" } return core.Trim(parts[len(parts)-1]) }