feat(agentic): add issue assignment action
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
ae910099c8
commit
dfaf14f061
4 changed files with 118 additions and 2 deletions
|
|
@ -20,6 +20,7 @@ type Issue struct {
|
|||
Type string `json:"type,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Priority string `json:"priority,omitempty"`
|
||||
Assignee string `json:"assignee,omitempty"`
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
|
|
@ -43,6 +44,7 @@ type IssueCreateInput struct {
|
|||
Type string `json:"type,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Priority string `json:"priority,omitempty"`
|
||||
Assignee string `json:"assignee,omitempty"`
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
SprintID int `json:"sprint_id,omitempty"`
|
||||
SprintSlug string `json:"sprint_slug,omitempty"`
|
||||
|
|
@ -72,11 +74,19 @@ type IssueUpdateInput struct {
|
|||
Type string `json:"type,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Priority string `json:"priority,omitempty"`
|
||||
Assignee string `json:"assignee,omitempty"`
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
SprintID int `json:"sprint_id,omitempty"`
|
||||
SprintSlug string `json:"sprint_slug,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.IssueAssignInput{Slug: "fix-auth", Assignee: "codex"}
|
||||
type IssueAssignInput struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Assignee string `json:"assignee,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.IssueCommentInput{Slug: "fix-auth", Body: "Ready for review"}
|
||||
type IssueCommentInput struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
|
|
@ -126,6 +136,7 @@ func (s *PrepSubsystem) handleIssueRecordCreate(ctx context.Context, options cor
|
|||
Type: optionStringValue(options, "type"),
|
||||
Status: optionStringValue(options, "status"),
|
||||
Priority: optionStringValue(options, "priority"),
|
||||
Assignee: optionStringValue(options, "assignee"),
|
||||
Labels: optionStringSliceValue(options, "labels"),
|
||||
SprintID: optionIntValue(options, "sprint_id", "sprint-id"),
|
||||
SprintSlug: optionStringValue(options, "sprint_slug", "sprint-slug"),
|
||||
|
|
@ -173,6 +184,7 @@ func (s *PrepSubsystem) handleIssueRecordUpdate(ctx context.Context, options cor
|
|||
Type: optionStringValue(options, "type"),
|
||||
Status: optionStringValue(options, "status"),
|
||||
Priority: optionStringValue(options, "priority"),
|
||||
Assignee: optionStringValue(options, "assignee"),
|
||||
Labels: optionStringSliceValue(options, "labels"),
|
||||
SprintID: optionIntValue(options, "sprint_id", "sprint-id"),
|
||||
SprintSlug: optionStringValue(options, "sprint_slug", "sprint-slug"),
|
||||
|
|
@ -183,6 +195,24 @@ func (s *PrepSubsystem) handleIssueRecordUpdate(ctx context.Context, options cor
|
|||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("issue.assign").Run(ctx, core.NewOptions(
|
||||
//
|
||||
// core.Option{Key: "slug", Value: "fix-auth"},
|
||||
// core.Option{Key: "assignee", Value: "codex"},
|
||||
//
|
||||
// ))
|
||||
func (s *PrepSubsystem) handleIssueRecordAssign(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.issueAssign(ctx, nil, IssueAssignInput{
|
||||
ID: optionStringValue(options, "id", "_arg"),
|
||||
Slug: optionStringValue(options, "slug"),
|
||||
Assignee: optionStringValue(options, "assignee", "agent", "agent_type"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{Value: output, OK: true}
|
||||
}
|
||||
|
||||
// result := c.Action("issue.comment").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "fix-auth"}))
|
||||
func (s *PrepSubsystem) handleIssueRecordComment(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.issueComment(ctx, nil, IssueCommentInput{
|
||||
|
|
@ -232,6 +262,11 @@ func (s *PrepSubsystem) registerIssueTools(server *mcp.Server) {
|
|||
Description: "Update fields on a tracked platform issue by slug.",
|
||||
}, s.issueUpdate)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "issue_assign",
|
||||
Description: "Assign an agent or user to a tracked platform issue by slug.",
|
||||
}, s.issueAssign)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "issue_comment",
|
||||
Description: "Add a comment to a tracked platform issue.",
|
||||
|
|
@ -263,6 +298,9 @@ func (s *PrepSubsystem) issueCreate(ctx context.Context, _ *mcp.CallToolRequest,
|
|||
if input.Priority != "" {
|
||||
body["priority"] = input.Priority
|
||||
}
|
||||
if input.Assignee != "" {
|
||||
body["assignee"] = input.Assignee
|
||||
}
|
||||
if len(input.Labels) > 0 {
|
||||
body["labels"] = input.Labels
|
||||
}
|
||||
|
|
@ -343,6 +381,9 @@ func (s *PrepSubsystem) issueUpdate(ctx context.Context, _ *mcp.CallToolRequest,
|
|||
if input.Priority != "" {
|
||||
body["priority"] = input.Priority
|
||||
}
|
||||
if input.Assignee != "" {
|
||||
body["assignee"] = input.Assignee
|
||||
}
|
||||
if len(input.Labels) > 0 {
|
||||
body["labels"] = input.Labels
|
||||
}
|
||||
|
|
@ -367,6 +408,22 @@ func (s *PrepSubsystem) issueUpdate(ctx context.Context, _ *mcp.CallToolRequest,
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) issueAssign(ctx context.Context, _ *mcp.CallToolRequest, input IssueAssignInput) (*mcp.CallToolResult, IssueOutput, error) {
|
||||
identifier := issueRecordIdentifier(input.Slug, input.ID)
|
||||
if identifier == "" {
|
||||
return nil, IssueOutput{}, core.E("issueAssign", "id or slug is required", nil)
|
||||
}
|
||||
if input.Assignee == "" {
|
||||
return nil, IssueOutput{}, core.E("issueAssign", "assignee is required", nil)
|
||||
}
|
||||
|
||||
return s.issueUpdate(ctx, nil, IssueUpdateInput{
|
||||
ID: input.ID,
|
||||
Slug: input.Slug,
|
||||
Assignee: input.Assignee,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) issueComment(ctx context.Context, _ *mcp.CallToolRequest, input IssueCommentInput) (*mcp.CallToolResult, IssueCommentOutput, error) {
|
||||
identifier := issueRecordIdentifier(input.Slug, input.IssueID, input.ID)
|
||||
if identifier == "" {
|
||||
|
|
@ -434,6 +491,7 @@ func parseIssue(values map[string]any) Issue {
|
|||
Type: stringValue(values["type"]),
|
||||
Status: stringValue(values["status"]),
|
||||
Priority: stringValue(values["priority"]),
|
||||
Assignee: stringValue(values["assignee"]),
|
||||
Labels: listValue(values["labels"]),
|
||||
Metadata: anyMapValue(values["metadata"]),
|
||||
CreatedAt: stringValue(values["created_at"]),
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ func TestIssue_HandleIssueRecordCreate_Good(t *testing.T) {
|
|||
require.True(t, parseResult.OK)
|
||||
require.Equal(t, "Fix auth", payload["title"])
|
||||
require.Equal(t, "bug", payload["type"])
|
||||
require.Equal(t, "codex", payload["assignee"])
|
||||
|
||||
_, _ = w.Write([]byte(`{"data":{"slug":"fix-auth","title":"Fix auth","type":"bug","status":"open","priority":"high","labels":["auth"]}}`))
|
||||
_, _ = w.Write([]byte(`{"data":{"slug":"fix-auth","title":"Fix auth","type":"bug","status":"open","priority":"high","assignee":"codex","labels":["auth"]}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ func TestIssue_HandleIssueRecordCreate_Good(t *testing.T) {
|
|||
core.Option{Key: "title", Value: "Fix auth"},
|
||||
core.Option{Key: "type", Value: "bug"},
|
||||
core.Option{Key: "priority", Value: "high"},
|
||||
core.Option{Key: "assignee", Value: "codex"},
|
||||
core.Option{Key: "labels", Value: "auth"},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
|
@ -45,6 +47,7 @@ func TestIssue_HandleIssueRecordCreate_Good(t *testing.T) {
|
|||
assert.True(t, output.Success)
|
||||
assert.Equal(t, "fix-auth", output.Issue.Slug)
|
||||
assert.Equal(t, "open", output.Issue.Status)
|
||||
assert.Equal(t, "codex", output.Issue.Assignee)
|
||||
assert.Equal(t, []string{"auth"}, output.Issue.Labels)
|
||||
}
|
||||
|
||||
|
|
@ -75,6 +78,57 @@ func TestIssue_HandleIssueRecordGet_Good_IDAlias(t *testing.T) {
|
|||
assert.Equal(t, "fix-auth", output.Issue.Slug)
|
||||
}
|
||||
|
||||
func TestIssue_HandleIssueRecordAssign_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/issues/fix-auth", r.URL.Path)
|
||||
require.Equal(t, http.MethodPatch, r.Method)
|
||||
|
||||
bodyResult := core.ReadAll(r.Body)
|
||||
require.True(t, bodyResult.OK)
|
||||
|
||||
var payload map[string]any
|
||||
parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload)
|
||||
require.True(t, parseResult.OK)
|
||||
require.Equal(t, "codex", payload["assignee"])
|
||||
|
||||
_, _ = w.Write([]byte(`{"data":{"issue":{"slug":"fix-auth","title":"Fix auth","status":"assigned","assignee":"codex"}}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleIssueRecordAssign(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "slug", Value: "fix-auth"},
|
||||
core.Option{Key: "assignee", Value: "codex"},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(IssueOutput)
|
||||
require.True(t, ok)
|
||||
assert.True(t, output.Success)
|
||||
assert.Equal(t, "codex", output.Issue.Assignee)
|
||||
assert.Equal(t, "assigned", output.Issue.Status)
|
||||
}
|
||||
|
||||
func TestIssue_HandleIssueRecordAssign_Bad_MissingAssignee(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
|
||||
|
||||
result := subsystem.handleIssueRecordAssign(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "slug", Value: "fix-auth"},
|
||||
))
|
||||
assert.False(t, result.OK)
|
||||
assert.EqualError(t, result.Value.(error), "issueAssign: assignee is required")
|
||||
}
|
||||
|
||||
func TestIssue_HandleIssueRecordAssign_Ugly_MissingIdentifier(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
|
||||
|
||||
result := subsystem.handleIssueRecordAssign(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "assignee", Value: "codex"},
|
||||
))
|
||||
assert.False(t, result.OK)
|
||||
assert.EqualError(t, result.Value.(error), "issueAssign: id or slug is required")
|
||||
}
|
||||
|
||||
func TestIssue_HandleIssueRecordList_Ugly_NestedEnvelope(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/issues", r.URL.Path)
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result {
|
|||
}
|
||||
switch action {
|
||||
case "agentic.status", "agentic.scan", "agentic.watch",
|
||||
"agentic.issue.get", "agentic.issue.list", "agentic.pr.get", "agentic.pr.list",
|
||||
"agentic.issue.get", "agentic.issue.list", "agentic.issue.assign", "agentic.pr.get", "agentic.pr.list",
|
||||
"agentic.prompt", "agentic.task", "agentic.flow", "agentic.persona",
|
||||
"agentic.sync.status", "agentic.fleet.nodes", "agentic.fleet.stats", "agentic.fleet.events",
|
||||
"agentic.credits.balance", "agentic.credits.history",
|
||||
|
|
@ -219,8 +219,10 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result {
|
|||
c.Action("issue.get", s.handleIssueRecordGet).Description = "Read a tracked platform issue by slug"
|
||||
c.Action("issue.list", s.handleIssueRecordList).Description = "List tracked platform issues with optional filters"
|
||||
c.Action("issue.update", s.handleIssueRecordUpdate).Description = "Update a tracked platform issue by slug"
|
||||
c.Action("issue.assign", s.handleIssueRecordAssign).Description = "Assign an agent or user to a tracked platform issue"
|
||||
c.Action("issue.comment", s.handleIssueRecordComment).Description = "Add a comment to a tracked platform issue"
|
||||
c.Action("issue.archive", s.handleIssueRecordArchive).Description = "Archive a tracked platform issue by slug"
|
||||
c.Action("agentic.issue.assign", s.handleIssueRecordAssign).Description = "Assign an agent or user to a tracked platform issue"
|
||||
c.Action("sprint.create", s.handleSprintCreate).Description = "Create a tracked platform sprint"
|
||||
c.Action("sprint.get", s.handleSprintGet).Description = "Read a tracked platform sprint by slug"
|
||||
c.Action("sprint.list", s.handleSprintList).Description = "List tracked platform sprints with optional filters"
|
||||
|
|
|
|||
|
|
@ -512,9 +512,11 @@ func TestPrep_OnStartup_Good_RegistersSessionActions(t *testing.T) {
|
|||
assert.True(t, c.Action("issue.get").Exists())
|
||||
assert.True(t, c.Action("issue.list").Exists())
|
||||
assert.True(t, c.Action("issue.update").Exists())
|
||||
assert.True(t, c.Action("issue.assign").Exists())
|
||||
assert.True(t, c.Action("issue.comment").Exists())
|
||||
assert.True(t, c.Action("issue.archive").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.update").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.assign").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.comment").Exists())
|
||||
assert.True(t, c.Action("agentic.issue.archive").Exists())
|
||||
assert.True(t, c.Action("sprint.create").Exists())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue