229 lines
5.8 KiB
Go
229 lines
5.8 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package agentic
|
|
|
|
import (
|
|
"context"
|
|
|
|
core "dappco.re/go/core"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
type EpicInput struct {
|
|
Repo string `json:"repo"`
|
|
Org string `json:"org,omitempty"`
|
|
Title string `json:"title"`
|
|
Body string `json:"body,omitempty"`
|
|
Tasks []string `json:"tasks"`
|
|
Labels []string `json:"labels,omitempty"`
|
|
Dispatch bool `json:"dispatch,omitempty"`
|
|
Agent string `json:"agent,omitempty"`
|
|
Template string `json:"template,omitempty"`
|
|
}
|
|
|
|
type EpicOutput struct {
|
|
Success bool `json:"success"`
|
|
EpicNumber int `json:"epic_number"`
|
|
EpicURL string `json:"epic_url"`
|
|
Children []ChildRef `json:"children"`
|
|
Dispatched int `json:"dispatched,omitempty"`
|
|
}
|
|
|
|
type ChildRef struct {
|
|
Number int `json:"number"`
|
|
Title string `json:"title"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
func (s *PrepSubsystem) registerEpicTool(server *mcp.Server) {
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "agentic_create_epic",
|
|
Description: "Create an epic issue with child issues on Forge. Each task becomes a child issue linked via checklist. Optionally auto-dispatch agents to work each child.",
|
|
}, s.createEpic)
|
|
}
|
|
|
|
func (s *PrepSubsystem) createEpic(ctx context.Context, callRequest *mcp.CallToolRequest, input EpicInput) (*mcp.CallToolResult, EpicOutput, error) {
|
|
if input.Title == "" {
|
|
return nil, EpicOutput{}, core.E("createEpic", "title is required", nil)
|
|
}
|
|
if len(input.Tasks) == 0 {
|
|
return nil, EpicOutput{}, core.E("createEpic", "at least one task is required", nil)
|
|
}
|
|
if s.forgeToken == "" {
|
|
return nil, EpicOutput{}, core.E("createEpic", "no Forge token configured", nil)
|
|
}
|
|
if input.Org == "" {
|
|
input.Org = "core"
|
|
}
|
|
if input.Agent == "" {
|
|
input.Agent = "claude"
|
|
}
|
|
if input.Template == "" {
|
|
input.Template = "coding"
|
|
}
|
|
|
|
labels := input.Labels
|
|
hasAgentic := false
|
|
for _, l := range labels {
|
|
if l == "agentic" {
|
|
hasAgentic = true
|
|
break
|
|
}
|
|
}
|
|
if !hasAgentic {
|
|
labels = append(labels, "agentic")
|
|
}
|
|
|
|
labelIDs := s.resolveLabelIDs(ctx, input.Org, input.Repo, labels)
|
|
|
|
var children []ChildRef
|
|
for _, task := range input.Tasks {
|
|
child, err := s.createIssue(ctx, input.Org, input.Repo, task, "", labelIDs)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
children = append(children, child)
|
|
}
|
|
|
|
body := core.NewBuilder()
|
|
if input.Body != "" {
|
|
body.WriteString(input.Body)
|
|
body.WriteString("\n\n")
|
|
}
|
|
body.WriteString("## Tasks\n\n")
|
|
for _, child := range children {
|
|
body.WriteString(core.Sprintf("- [ ] #%d %s\n", child.Number, child.Title))
|
|
}
|
|
|
|
epicLabels := append(labelIDs, s.resolveLabelIDs(ctx, input.Org, input.Repo, []string{"epic"})...)
|
|
epic, err := s.createIssue(ctx, input.Org, input.Repo, input.Title, body.String(), epicLabels)
|
|
if err != nil {
|
|
return nil, EpicOutput{}, core.E("createEpic", "failed to create epic", err)
|
|
}
|
|
|
|
out := EpicOutput{
|
|
Success: true,
|
|
EpicNumber: epic.Number,
|
|
EpicURL: epic.URL,
|
|
Children: children,
|
|
}
|
|
|
|
if input.Dispatch {
|
|
for _, child := range children {
|
|
_, _, err := s.dispatch(ctx, callRequest, DispatchInput{
|
|
Repo: input.Repo,
|
|
Org: input.Org,
|
|
Task: child.Title,
|
|
Agent: input.Agent,
|
|
Template: input.Template,
|
|
Issue: child.Number,
|
|
})
|
|
if err == nil {
|
|
out.Dispatched++
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, out, nil
|
|
}
|
|
|
|
// child, err := s.createIssue(ctx, "core", "go-scm", "Port agentic plans", "", nil)
|
|
func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body string, labelIDs []int64) (ChildRef, error) {
|
|
payload := map[string]any{
|
|
"title": title,
|
|
}
|
|
if body != "" {
|
|
payload["body"] = body
|
|
}
|
|
if len(labelIDs) > 0 {
|
|
payload["labels"] = labelIDs
|
|
}
|
|
|
|
data := core.JSONMarshalString(payload)
|
|
url := core.Sprintf("%s/api/v1/repos/%s/%s/issues", s.forgeURL, org, repo)
|
|
httpResult := HTTPPost(ctx, url, data, s.forgeToken, "token")
|
|
if !httpResult.OK {
|
|
return ChildRef{}, core.E("createIssue", "create issue request failed", nil)
|
|
}
|
|
|
|
var createdIssue struct {
|
|
Number int `json:"number"`
|
|
HTMLURL string `json:"html_url"`
|
|
}
|
|
core.JSONUnmarshalString(httpResult.Value.(string), &createdIssue)
|
|
|
|
return ChildRef{
|
|
Number: createdIssue.Number,
|
|
Title: title,
|
|
URL: createdIssue.HTMLURL,
|
|
}, nil
|
|
}
|
|
|
|
// labelIDs := s.resolveLabelIDs(ctx, "core", "go-scm", []string{"agentic", "epic"})
|
|
func (s *PrepSubsystem) resolveLabelIDs(ctx context.Context, org, repo string, names []string) []int64 {
|
|
if len(names) == 0 {
|
|
return nil
|
|
}
|
|
|
|
url := core.Sprintf("%s/api/v1/repos/%s/%s/labels?limit=50", s.forgeURL, org, repo)
|
|
httpResult := HTTPGet(ctx, url, s.forgeToken, "token")
|
|
if !httpResult.OK {
|
|
return nil
|
|
}
|
|
|
|
var existing []struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
core.JSONUnmarshalString(httpResult.Value.(string), &existing)
|
|
|
|
nameToID := make(map[string]int64)
|
|
for _, l := range existing {
|
|
nameToID[l.Name] = l.ID
|
|
}
|
|
|
|
var ids []int64
|
|
for _, name := range names {
|
|
if id, ok := nameToID[name]; ok {
|
|
ids = append(ids, id)
|
|
} else {
|
|
id := s.createLabel(ctx, org, repo, name)
|
|
if id > 0 {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
return ids
|
|
}
|
|
|
|
// id := s.createLabel(ctx, "core", "go-scm", "agentic")
|
|
func (s *PrepSubsystem) createLabel(ctx context.Context, org, repo, name string) int64 {
|
|
colours := map[string]string{
|
|
"agentic": "#7c3aed",
|
|
"epic": "#dc2626",
|
|
"bug": "#ef4444",
|
|
"help-wanted": "#22c55e",
|
|
}
|
|
colour := colours[name]
|
|
if colour == "" {
|
|
colour = "#6b7280"
|
|
}
|
|
|
|
payload := core.JSONMarshalString(map[string]string{
|
|
"name": name,
|
|
"color": colour,
|
|
})
|
|
|
|
url := core.Sprintf("%s/api/v1/repos/%s/%s/labels", s.forgeURL, org, repo)
|
|
httpResult := HTTPPost(ctx, url, payload, s.forgeToken, "token")
|
|
if !httpResult.OK {
|
|
return 0
|
|
}
|
|
|
|
var createdLabel struct {
|
|
ID int64 `json:"id"`
|
|
}
|
|
core.JSONUnmarshalString(httpResult.Value.(string), &createdLabel)
|
|
return createdLabel.ID
|
|
}
|