agent/pkg/agentic/sprint.go
Snider 39914fbf14 refactor: AX compliance sweep — replace banned stdlib imports with core primitives
Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath,
errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim,
core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(),
core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives.

Framework boundary exceptions preserved where stdlib types are required
by external interfaces (Gin, net/http, CGo, Wails, bubbletea).

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-13 09:32:00 +01:00

382 lines
13 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
core "dappco.re/go/core"
coremcp "dappco.re/go/mcp/pkg/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// sprint := agentic.Sprint{Slug: "ax-follow-up", Title: "AX Follow-up", Status: "active"}
type Sprint struct {
ID int `json:"id"`
WorkspaceID int `json:"workspace_id,omitempty"`
Slug string `json:"slug"`
Title string `json:"title"`
Goal string `json:"goal,omitempty"`
Status string `json:"status,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
StartedAt string `json:"started_at,omitempty"`
EndedAt string `json:"ended_at,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
}
// input := agentic.SprintCreateInput{Title: "AX Follow-up", Goal: "Finish RFC parity"}
type SprintCreateInput struct {
Title string `json:"title"`
Goal string `json:"goal,omitempty"`
Status string `json:"status,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
StartedAt string `json:"started_at,omitempty"`
EndedAt string `json:"ended_at,omitempty"`
}
// input := agentic.SprintGetInput{Slug: "ax-follow-up"}
type SprintGetInput struct {
ID string `json:"id,omitempty"`
Slug string `json:"slug,omitempty"`
}
// input := agentic.SprintListInput{Status: "active", Limit: 10}
type SprintListInput struct {
Status string `json:"status,omitempty"`
Limit int `json:"limit,omitempty"`
}
// input := agentic.SprintUpdateInput{Slug: "ax-follow-up", Status: "completed"}
type SprintUpdateInput struct {
ID string `json:"id,omitempty"`
Slug string `json:"slug,omitempty"`
Title string `json:"title,omitempty"`
Goal string `json:"goal,omitempty"`
Status string `json:"status,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
StartedAt string `json:"started_at,omitempty"`
EndedAt string `json:"ended_at,omitempty"`
}
// input := agentic.SprintArchiveInput{Slug: "ax-follow-up"}
type SprintArchiveInput struct {
ID string `json:"id,omitempty"`
Slug string `json:"slug,omitempty"`
}
// out := agentic.SprintOutput{Success: true, Sprint: agentic.Sprint{Slug: "ax-follow-up"}}
type SprintOutput struct {
Success bool `json:"success"`
Sprint Sprint `json:"sprint"`
}
// out := agentic.SprintListOutput{Success: true, Count: 1, Sprints: []agentic.Sprint{{Slug: "ax-follow-up"}}}
type SprintListOutput struct {
Success bool `json:"success"`
Count int `json:"count"`
Sprints []Sprint `json:"sprints"`
}
// out := agentic.SprintArchiveOutput{Success: true, Archived: "ax-follow-up"}
type SprintArchiveOutput struct {
Success bool `json:"success"`
Archived string `json:"archived"`
}
// result := c.Action("sprint.create").Run(ctx, core.NewOptions(core.Option{Key: "title", Value: "AX Follow-up"}))
func (s *PrepSubsystem) handleSprintCreate(ctx context.Context, options core.Options) core.Result {
_, output, err := s.sprintCreate(ctx, nil, SprintCreateInput{
Title: optionStringValue(options, "title"),
Goal: optionStringValue(options, "goal"),
Status: optionStringValue(options, "status"),
Metadata: optionAnyMapValue(options, "metadata"),
StartedAt: optionStringValue(options, "started_at", "started-at"),
EndedAt: optionStringValue(options, "ended_at", "ended-at"),
})
if err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{Value: output, OK: true}
}
// result := c.Action("sprint.get").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "ax-follow-up"}))
func (s *PrepSubsystem) handleSprintGet(ctx context.Context, options core.Options) core.Result {
_, output, err := s.sprintGet(ctx, nil, SprintGetInput{
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("sprint.list").Run(ctx, core.NewOptions(core.Option{Key: "status", Value: "active"}))
func (s *PrepSubsystem) handleSprintList(ctx context.Context, options core.Options) core.Result {
_, output, err := s.sprintList(ctx, nil, SprintListInput{
Status: optionStringValue(options, "status"),
Limit: optionIntValue(options, "limit"),
})
if err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{Value: output, OK: true}
}
// result := c.Action("sprint.update").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "ax-follow-up"}))
func (s *PrepSubsystem) handleSprintUpdate(ctx context.Context, options core.Options) core.Result {
_, output, err := s.sprintUpdate(ctx, nil, SprintUpdateInput{
ID: optionStringValue(options, "id", "_arg"),
Slug: optionStringValue(options, "slug"),
Title: optionStringValue(options, "title"),
Goal: optionStringValue(options, "goal"),
Status: optionStringValue(options, "status"),
Metadata: optionAnyMapValue(options, "metadata"),
StartedAt: optionStringValue(options, "started_at", "started-at"),
EndedAt: optionStringValue(options, "ended_at", "ended-at"),
})
if err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{Value: output, OK: true}
}
// result := c.Action("sprint.archive").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "ax-follow-up"}))
func (s *PrepSubsystem) handleSprintArchive(ctx context.Context, options core.Options) core.Result {
_, output, err := s.sprintArchive(ctx, nil, SprintArchiveInput{
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}
}
func (s *PrepSubsystem) registerSprintTools(svc *coremcp.Service) {
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "sprint_create",
Description: "Create a tracked platform sprint with goal, schedule, and metadata.",
}, s.sprintCreate)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "agentic_sprint_create",
Description: "Create a tracked platform sprint with goal, schedule, and metadata.",
}, s.sprintCreate)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "sprint_get",
Description: "Read a tracked platform sprint by slug.",
}, s.sprintGet)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "agentic_sprint_get",
Description: "Read a tracked platform sprint by slug.",
}, s.sprintGet)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "sprint_list",
Description: "List tracked platform sprints with optional status and limit filters.",
}, s.sprintList)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "agentic_sprint_list",
Description: "List tracked platform sprints with optional status and limit filters.",
}, s.sprintList)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "sprint_update",
Description: "Update fields on a tracked platform sprint by slug.",
}, s.sprintUpdate)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "agentic_sprint_update",
Description: "Update fields on a tracked platform sprint by slug.",
}, s.sprintUpdate)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "sprint_archive",
Description: "Archive a tracked platform sprint by slug.",
}, s.sprintArchive)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "agentic_sprint_archive",
Description: "Archive a tracked platform sprint by slug.",
}, s.sprintArchive)
}
func (s *PrepSubsystem) sprintCreate(ctx context.Context, _ *mcp.CallToolRequest, input SprintCreateInput) (*mcp.CallToolResult, SprintOutput, error) {
if input.Title == "" {
return nil, SprintOutput{}, core.E("sprintCreate", "title is required", nil)
}
body := map[string]any{
"title": input.Title,
}
if input.Goal != "" {
body["goal"] = input.Goal
}
if input.Status != "" {
body["status"] = input.Status
}
if len(input.Metadata) > 0 {
body["metadata"] = input.Metadata
}
if input.StartedAt != "" {
body["started_at"] = input.StartedAt
}
if input.EndedAt != "" {
body["ended_at"] = input.EndedAt
}
result := s.platformPayload(ctx, "sprint.create", "POST", "/v1/sprints", body)
if !result.OK {
return nil, SprintOutput{}, resultErrorValue("sprint.create", result)
}
return nil, SprintOutput{
Success: true,
Sprint: parseSprint(payloadResourceMap(result.Value.(map[string]any), "sprint")),
}, nil
}
func (s *PrepSubsystem) sprintGet(ctx context.Context, _ *mcp.CallToolRequest, input SprintGetInput) (*mcp.CallToolResult, SprintOutput, error) {
identifier := sprintIdentifier(input.Slug, input.ID)
if identifier == "" {
return nil, SprintOutput{}, core.E("sprintGet", "id or slug is required", nil)
}
result := s.platformPayload(ctx, "sprint.get", "GET", core.Concat("/v1/sprints/", identifier), nil)
if !result.OK {
return nil, SprintOutput{}, resultErrorValue("sprint.get", result)
}
return nil, SprintOutput{
Success: true,
Sprint: parseSprint(payloadResourceMap(result.Value.(map[string]any), "sprint")),
}, nil
}
func (s *PrepSubsystem) sprintList(ctx context.Context, _ *mcp.CallToolRequest, input SprintListInput) (*mcp.CallToolResult, SprintListOutput, error) {
path := "/v1/sprints"
path = appendQueryParam(path, "status", input.Status)
if input.Limit > 0 {
path = appendQueryParam(path, "limit", core.Sprint(input.Limit))
}
result := s.platformPayload(ctx, "sprint.list", "GET", path, nil)
if !result.OK {
return nil, SprintListOutput{}, resultErrorValue("sprint.list", result)
}
return nil, parseSprintListOutput(result.Value.(map[string]any)), nil
}
func (s *PrepSubsystem) sprintUpdate(ctx context.Context, _ *mcp.CallToolRequest, input SprintUpdateInput) (*mcp.CallToolResult, SprintOutput, error) {
identifier := sprintIdentifier(input.Slug, input.ID)
if identifier == "" {
return nil, SprintOutput{}, core.E("sprintUpdate", "id or slug is required", nil)
}
body := map[string]any{}
if input.Title != "" {
body["title"] = input.Title
}
if input.Goal != "" {
body["goal"] = input.Goal
}
if input.Status != "" {
body["status"] = input.Status
}
if len(input.Metadata) > 0 {
body["metadata"] = input.Metadata
}
if input.StartedAt != "" {
body["started_at"] = input.StartedAt
}
if input.EndedAt != "" {
body["ended_at"] = input.EndedAt
}
if len(body) == 0 {
return nil, SprintOutput{}, core.E("sprintUpdate", "at least one field is required", nil)
}
result := s.platformPayload(ctx, "sprint.update", "PATCH", core.Concat("/v1/sprints/", identifier), body)
if !result.OK {
return nil, SprintOutput{}, resultErrorValue("sprint.update", result)
}
return nil, SprintOutput{
Success: true,
Sprint: parseSprint(payloadResourceMap(result.Value.(map[string]any), "sprint")),
}, nil
}
func (s *PrepSubsystem) sprintArchive(ctx context.Context, _ *mcp.CallToolRequest, input SprintArchiveInput) (*mcp.CallToolResult, SprintArchiveOutput, error) {
identifier := sprintIdentifier(input.Slug, input.ID)
if identifier == "" {
return nil, SprintArchiveOutput{}, core.E("sprintArchive", "id or slug is required", nil)
}
result := s.platformPayload(ctx, "sprint.archive", "DELETE", core.Concat("/v1/sprints/", identifier), nil)
if !result.OK {
return nil, SprintArchiveOutput{}, resultErrorValue("sprint.archive", result)
}
output := SprintArchiveOutput{
Success: true,
Archived: identifier,
}
if values := payloadResourceMap(result.Value.(map[string]any), "sprint", "result"); len(values) > 0 {
if slug := stringValue(values["slug"]); slug != "" {
output.Archived = slug
}
if value, ok := boolValueOK(values["success"]); ok {
output.Success = value
}
}
return nil, output, nil
}
func sprintIdentifier(values ...string) string {
for _, value := range values {
if trimmed := core.Trim(value); trimmed != "" {
return trimmed
}
}
return ""
}
func parseSprint(values map[string]any) Sprint {
return Sprint{
ID: intValue(values["id"]),
WorkspaceID: intValue(values["workspace_id"]),
Slug: stringValue(values["slug"]),
Title: stringValue(values["title"]),
Goal: stringValue(values["goal"]),
Status: stringValue(values["status"]),
Metadata: anyMapValue(values["metadata"]),
StartedAt: stringValue(values["started_at"]),
EndedAt: stringValue(values["ended_at"]),
CreatedAt: stringValue(values["created_at"]),
UpdatedAt: stringValue(values["updated_at"]),
}
}
func parseSprintListOutput(payload map[string]any) SprintListOutput {
sprintsData := payloadDataSlice(payload, "sprints")
sprints := make([]Sprint, 0, len(sprintsData))
for _, values := range sprintsData {
sprints = append(sprints, parseSprint(values))
}
count := mapIntValue(payload, "total", "count")
if count == 0 {
count = mapIntValue(payloadDataMap(payload), "total", "count")
}
if count == 0 {
count = len(sprints)
}
return SprintListOutput{
Success: true,
Count: count,
Sprints: sprints,
}
}