feat(mcp): include associated PRs in task-listing tools (#1236)
This commit is contained in:
@@ -895,6 +895,45 @@ func (a githubRateLimitAdapter) ExhaustedRateLimits() []health.GitHubRateLimitSt
|
||||
return out
|
||||
}
|
||||
|
||||
// mcpTaskPRListerAdapter adapts *github.Service to the MCP handlers'
|
||||
// TaskPRLister interface so list_tasks responses can carry per-task PR
|
||||
// summaries. Returns an empty map when the github service is nil.
|
||||
type mcpTaskPRListerAdapter struct {
|
||||
gh *github.Service
|
||||
}
|
||||
|
||||
func (a mcpTaskPRListerAdapter) ListTaskPRsByTaskIDs(
|
||||
ctx context.Context, taskIDs []string,
|
||||
) (map[string][]mcphandlers.TaskPRInfo, error) {
|
||||
out := make(map[string][]mcphandlers.TaskPRInfo)
|
||||
if a.gh == nil || len(taskIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
prs, err := a.gh.ListTaskPRsByTaskIDs(ctx, taskIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for taskID, list := range prs {
|
||||
infos := make([]mcphandlers.TaskPRInfo, 0, len(list))
|
||||
for _, pr := range list {
|
||||
if pr == nil {
|
||||
continue
|
||||
}
|
||||
infos = append(infos, mcphandlers.TaskPRInfo{
|
||||
Number: pr.PRNumber,
|
||||
URL: pr.PRURL,
|
||||
Title: pr.PRTitle,
|
||||
State: pr.State,
|
||||
MergedAt: pr.MergedAt,
|
||||
})
|
||||
}
|
||||
if len(infos) > 0 {
|
||||
out[taskID] = infos
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// registerMCPAndDebugRoutes registers MCP and debug routes and wires the MCP handler.
|
||||
func registerMCPAndDebugRoutes(
|
||||
p routeParams,
|
||||
@@ -911,6 +950,12 @@ func registerMCPAndDebugRoutes(
|
||||
// Wire config-mode dependencies for agent-native configuration
|
||||
mcpHandlers.SetConfigDeps(p.services.Workflow, p.agentSettingsController, p.mcpConfigSvc)
|
||||
|
||||
// Enrich list_tasks responses with associated GitHub PRs (link, title,
|
||||
// number, state) when the github service is available.
|
||||
if p.services.GitHub != nil {
|
||||
mcpHandlers.SetTaskPRLister(mcpTaskPRListerAdapter{gh: p.services.GitHub})
|
||||
}
|
||||
|
||||
// Reuse the cross-task handoff service constructed in registerRoutes —
|
||||
// the same instance backs the MCP path and the HTTP Kanban path so
|
||||
// workspace-group state stays consistent across both surfaces.
|
||||
|
||||
@@ -110,6 +110,10 @@ type Handlers struct {
|
||||
// Wires the list_related_tasks_kandev / *_task_document_kandev
|
||||
// MCP tools introduced in office task handoffs phase 2.
|
||||
handoffSvc *service.HandoffService
|
||||
|
||||
// Optional PR lister (set via SetTaskPRLister) used to enrich
|
||||
// task-listing responses with associated pull requests.
|
||||
taskPRLister TaskPRLister
|
||||
}
|
||||
|
||||
// NewHandlers creates new MCP handlers.
|
||||
@@ -352,6 +356,7 @@ func (h *Handlers) handleListTasks(ctx context.Context, msg *ws.Message) (*ws.Me
|
||||
for _, t := range tasks {
|
||||
dtos = append(dtos, dto.FromTask(t))
|
||||
}
|
||||
h.enrichTasksWithPRs(ctx, dtos)
|
||||
return dto.ListTasksResponse{Tasks: dtos, Total: len(dtos)}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ func (h *Handlers) handleListRelatedTasks(ctx context.Context, msg *ws.Message)
|
||||
h.logger.Error("list related tasks", zap.Error(err))
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, err.Error(), nil)
|
||||
}
|
||||
h.enrichRelatedTasksWithPRs(ctx, related)
|
||||
return ws.NewResponse(msg.ID, msg.Action, related)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/kandev/kandev/internal/task/dto"
|
||||
"github.com/kandev/kandev/internal/task/service"
|
||||
v1 "github.com/kandev/kandev/pkg/api/v1"
|
||||
)
|
||||
|
||||
// TaskPRInfo is the decoupled view of a task↔PR association consumed by the
|
||||
// MCP task-listing handlers. The cmd wiring adapts *github.Service.TaskPR into
|
||||
// this shape so this package does not depend on internal/github.
|
||||
type TaskPRInfo struct {
|
||||
Number int
|
||||
URL string
|
||||
Title string
|
||||
State string // open, closed, merged
|
||||
MergedAt *time.Time
|
||||
}
|
||||
|
||||
// TaskPRLister returns PR associations grouped by task ID. Backed by
|
||||
// *github.Service in production; left nil in contexts without GitHub, in which
|
||||
// case PRs are simply omitted from task responses.
|
||||
type TaskPRLister interface {
|
||||
ListTaskPRsByTaskIDs(ctx context.Context, taskIDs []string) (map[string][]TaskPRInfo, error)
|
||||
}
|
||||
|
||||
// SetTaskPRLister wires the optional PR lister used to enrich task-listing
|
||||
// responses with associated pull requests.
|
||||
func (h *Handlers) SetTaskPRLister(l TaskPRLister) {
|
||||
h.taskPRLister = l
|
||||
}
|
||||
|
||||
// prsByTaskID looks up PR associations for the given task IDs via the wired
|
||||
// lister, mapping each to the shared v1.TaskPRSummary shape. Returns nil when
|
||||
// the lister is unset, there are no IDs, or the lookup fails (logged) — callers
|
||||
// treat a nil map as "no PRs to attach".
|
||||
func (h *Handlers) prsByTaskID(ctx context.Context, taskIDs []string) map[string][]v1.TaskPRSummary {
|
||||
if h.taskPRLister == nil || len(taskIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
byTask, err := h.taskPRLister.ListTaskPRsByTaskIDs(ctx, taskIDs)
|
||||
if err != nil {
|
||||
h.logger.Warn("failed to list task PRs for MCP response", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
out := make(map[string][]v1.TaskPRSummary, len(byTask))
|
||||
for taskID, prs := range byTask {
|
||||
if len(prs) == 0 {
|
||||
continue
|
||||
}
|
||||
summaries := make([]v1.TaskPRSummary, 0, len(prs))
|
||||
for _, pr := range prs {
|
||||
summaries = append(summaries, v1.TaskPRSummary{
|
||||
Number: pr.Number,
|
||||
URL: pr.URL,
|
||||
Title: pr.Title,
|
||||
State: pr.State,
|
||||
MergedAt: pr.MergedAt,
|
||||
})
|
||||
}
|
||||
out[taskID] = summaries
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// enrichTasksWithPRs populates dto.TaskDTO.PRs for each task from the wired
|
||||
// TaskPRLister. No-op when the lister is unset or there are no tasks.
|
||||
func (h *Handlers) enrichTasksWithPRs(ctx context.Context, tasks []dto.TaskDTO) {
|
||||
if h.taskPRLister == nil || len(tasks) == 0 {
|
||||
return
|
||||
}
|
||||
ids := make([]string, len(tasks))
|
||||
for i := range tasks {
|
||||
ids[i] = tasks[i].ID
|
||||
}
|
||||
byTask := h.prsByTaskID(ctx, ids)
|
||||
for i := range tasks {
|
||||
if prs := byTask[tasks[i].ID]; len(prs) > 0 {
|
||||
tasks[i].PRs = prs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// enrichRelatedTasksWithPRs populates RelatedTask.PRs across every relation
|
||||
// surface (the task itself, parent, children, siblings, blockers, blocked-by)
|
||||
// in a single batched lookup. No-op when the lister is unset.
|
||||
func (h *Handlers) enrichRelatedTasksWithPRs(ctx context.Context, related *service.RelatedTasks) {
|
||||
if h.taskPRLister == nil || related == nil {
|
||||
return
|
||||
}
|
||||
nodes := []*service.RelatedTask{&related.Task, related.Parent}
|
||||
nodes = append(nodes, related.Children...)
|
||||
nodes = append(nodes, related.Siblings...)
|
||||
nodes = append(nodes, related.Blockers...)
|
||||
nodes = append(nodes, related.BlockedBy...)
|
||||
|
||||
// A task can appear in more than one relation group (e.g. both a sibling
|
||||
// and a blocker), so dedup IDs before the lookup to avoid sending the same
|
||||
// value twice to the lister's WHERE id IN (...) query.
|
||||
ids := make([]string, 0, len(nodes))
|
||||
seen := make(map[string]struct{}, len(nodes))
|
||||
for _, n := range nodes {
|
||||
if n == nil {
|
||||
continue
|
||||
}
|
||||
if _, dup := seen[n.ID]; dup {
|
||||
continue
|
||||
}
|
||||
seen[n.ID] = struct{}{}
|
||||
ids = append(ids, n.ID)
|
||||
}
|
||||
byTask := h.prsByTaskID(ctx, ids)
|
||||
if byTask == nil {
|
||||
return
|
||||
}
|
||||
for _, n := range nodes {
|
||||
if n == nil {
|
||||
continue
|
||||
}
|
||||
if prs := byTask[n.ID]; len(prs) > 0 {
|
||||
n.PRs = prs
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kandev/kandev/internal/task/dto"
|
||||
"github.com/kandev/kandev/internal/task/models"
|
||||
"github.com/kandev/kandev/internal/task/service"
|
||||
v1 "github.com/kandev/kandev/pkg/api/v1"
|
||||
ws "github.com/kandev/kandev/pkg/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeTaskPRLister struct {
|
||||
byTask map[string][]TaskPRInfo
|
||||
err error
|
||||
gotIDs []string
|
||||
}
|
||||
|
||||
func (f *fakeTaskPRLister) ListTaskPRsByTaskIDs(_ context.Context, taskIDs []string) (map[string][]TaskPRInfo, error) {
|
||||
f.gotIDs = taskIDs
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
return f.byTask, nil
|
||||
}
|
||||
|
||||
func TestEnrichTasksWithPRs(t *testing.T) {
|
||||
lister := &fakeTaskPRLister{
|
||||
byTask: map[string][]TaskPRInfo{
|
||||
"task-1": {
|
||||
{Number: 42, URL: "https://github.com/o/r/pull/42", Title: "Fix bug", State: "merged"},
|
||||
{Number: 43, URL: "https://github.com/o/r/pull/43", Title: "Add feature", State: "open"},
|
||||
},
|
||||
},
|
||||
}
|
||||
h := &Handlers{taskPRLister: lister, logger: testLogger(t).WithFields()}
|
||||
|
||||
dtos := []dto.TaskDTO{{ID: "task-1"}, {ID: "task-2"}}
|
||||
h.enrichTasksWithPRs(context.Background(), dtos)
|
||||
|
||||
assert.ElementsMatch(t, []string{"task-1", "task-2"}, lister.gotIDs)
|
||||
require.Len(t, dtos[0].PRs, 2)
|
||||
assert.Equal(t, v1.TaskPRSummary{
|
||||
Number: 42, URL: "https://github.com/o/r/pull/42", Title: "Fix bug", State: "merged",
|
||||
}, dtos[0].PRs[0])
|
||||
assert.Equal(t, "open", dtos[0].PRs[1].State)
|
||||
assert.Nil(t, dtos[1].PRs, "tasks without PRs stay nil")
|
||||
}
|
||||
|
||||
func TestEnrichTasksWithPRs_NilListerIsNoop(t *testing.T) {
|
||||
h := &Handlers{logger: testLogger(t).WithFields()}
|
||||
dtos := []dto.TaskDTO{{ID: "task-1"}}
|
||||
h.enrichTasksWithPRs(context.Background(), dtos)
|
||||
assert.Nil(t, dtos[0].PRs)
|
||||
}
|
||||
|
||||
func TestHandleListTasks_IncludesAssociatedPRs(t *testing.T) {
|
||||
svc, repo := newTestTaskService(t)
|
||||
ctx := context.Background()
|
||||
now := time.Now().UTC()
|
||||
|
||||
require.NoError(t, repo.CreateWorkspace(ctx, &models.Workspace{
|
||||
ID: "ws-pr", Name: "PR WS", CreatedAt: now, UpdatedAt: now,
|
||||
}))
|
||||
require.NoError(t, repo.CreateTask(ctx, &models.Task{
|
||||
ID: "task-pr", WorkspaceID: "ws-pr", WorkflowID: "wf-pr",
|
||||
Title: "Has a PR", State: v1.TaskStateReview, CreatedAt: now, UpdatedAt: now,
|
||||
}))
|
||||
|
||||
h := &Handlers{
|
||||
taskSvc: svc,
|
||||
logger: testLogger(t).WithFields(),
|
||||
taskPRLister: &fakeTaskPRLister{byTask: map[string][]TaskPRInfo{
|
||||
"task-pr": {{Number: 7, URL: "https://github.com/o/r/pull/7", Title: "Ship it", State: "merged"}},
|
||||
}},
|
||||
}
|
||||
|
||||
msg := makeWSMessage(t, ws.ActionMCPListTasks, map[string]any{"workflow_id": "wf-pr"})
|
||||
resp, err := h.handleListTasks(ctx, msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var payload dto.ListTasksResponse
|
||||
require.NoError(t, json.Unmarshal(resp.Payload, &payload))
|
||||
require.Len(t, payload.Tasks, 1)
|
||||
require.Len(t, payload.Tasks[0].PRs, 1)
|
||||
assert.Equal(t, "merged", payload.Tasks[0].PRs[0].State)
|
||||
assert.Equal(t, 7, payload.Tasks[0].PRs[0].Number)
|
||||
assert.Equal(t, "https://github.com/o/r/pull/7", payload.Tasks[0].PRs[0].URL)
|
||||
}
|
||||
|
||||
func TestHandleListRelatedTasks_IncludesAssociatedPRs(t *testing.T) {
|
||||
svc, repo := newTestTaskService(t)
|
||||
ctx := context.Background()
|
||||
now := time.Now().UTC()
|
||||
|
||||
require.NoError(t, repo.CreateWorkspace(ctx, &models.Workspace{
|
||||
ID: "ws-rel", Name: "Rel WS", CreatedAt: now, UpdatedAt: now,
|
||||
}))
|
||||
require.NoError(t, repo.CreateTask(ctx, &models.Task{
|
||||
ID: "task-rel", WorkspaceID: "ws-rel", WorkflowID: "wf-rel",
|
||||
Title: "Self", State: v1.TaskStateReview, CreatedAt: now, UpdatedAt: now,
|
||||
}))
|
||||
|
||||
merged := now
|
||||
h := &Handlers{
|
||||
taskSvc: svc,
|
||||
handoffSvc: service.NewHandoffService(repo, nil, nil, nil, nil, testLogger(t)),
|
||||
logger: testLogger(t).WithFields(),
|
||||
taskPRLister: &fakeTaskPRLister{byTask: map[string][]TaskPRInfo{
|
||||
"task-rel": {{Number: 99, URL: "https://github.com/o/r/pull/99", Title: "Fix", State: "merged", MergedAt: &merged}},
|
||||
}},
|
||||
}
|
||||
|
||||
msg := makeWSMessage(t, ws.ActionMCPListRelatedTasks, map[string]any{"task_id": "task-rel"})
|
||||
resp, err := h.handleListRelatedTasks(ctx, msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var payload service.RelatedTasks
|
||||
require.NoError(t, json.Unmarshal(resp.Payload, &payload))
|
||||
require.Len(t, payload.Task.PRs, 1)
|
||||
assert.Equal(t, 99, payload.Task.PRs[0].Number)
|
||||
assert.Equal(t, "merged", payload.Task.PRs[0].State)
|
||||
require.NotNil(t, payload.Task.PRs[0].MergedAt, "merged_at should be surfaced")
|
||||
}
|
||||
|
||||
func TestEnrichRelatedTasksWithPRs(t *testing.T) {
|
||||
lister := &fakeTaskPRLister{byTask: map[string][]TaskPRInfo{
|
||||
"self": {{Number: 1, URL: "u1", State: "open"}},
|
||||
"parent": {{Number: 3, URL: "u3", State: "closed"}},
|
||||
"child": {{Number: 2, URL: "u2", State: "merged"}},
|
||||
}}
|
||||
h := &Handlers{taskPRLister: lister, logger: testLogger(t).WithFields()}
|
||||
|
||||
related := &service.RelatedTasks{
|
||||
Task: service.RelatedTask{ID: "self"},
|
||||
Parent: &service.RelatedTask{ID: "parent"},
|
||||
Children: []*service.RelatedTask{{ID: "child"}, {ID: "child-no-pr"}},
|
||||
Siblings: []*service.RelatedTask{{ID: "sibling"}},
|
||||
BlockedBy: []*service.RelatedTask{{ID: "blocker"}},
|
||||
}
|
||||
h.enrichRelatedTasksWithPRs(context.Background(), related)
|
||||
|
||||
assert.ElementsMatch(t,
|
||||
[]string{"self", "parent", "child", "child-no-pr", "sibling", "blocker"},
|
||||
lister.gotIDs)
|
||||
require.Len(t, related.Task.PRs, 1)
|
||||
assert.Equal(t, "open", related.Task.PRs[0].State)
|
||||
require.Len(t, related.Parent.PRs, 1)
|
||||
assert.Equal(t, "closed", related.Parent.PRs[0].State)
|
||||
require.Len(t, related.Children[0].PRs, 1)
|
||||
assert.Equal(t, "merged", related.Children[0].PRs[0].State)
|
||||
assert.Nil(t, related.Children[1].PRs, "child without PRs stays nil")
|
||||
assert.Nil(t, related.Siblings[0].PRs)
|
||||
}
|
||||
|
||||
func TestEnrichRelatedTasksWithPRs_NilSafe(t *testing.T) {
|
||||
// nil lister: no-op, no panic.
|
||||
h := &Handlers{logger: testLogger(t).WithFields()}
|
||||
h.enrichRelatedTasksWithPRs(context.Background(), &service.RelatedTasks{})
|
||||
// lister set but nil related: no panic.
|
||||
h2 := &Handlers{taskPRLister: &fakeTaskPRLister{}, logger: testLogger(t).WithFields()}
|
||||
h2.enrichRelatedTasksWithPRs(context.Background(), nil)
|
||||
}
|
||||
|
||||
func TestEnrichTasksWithPRs_ListerErrorIsSwallowed(t *testing.T) {
|
||||
h := &Handlers{
|
||||
taskPRLister: &fakeTaskPRLister{err: errors.New("boom")},
|
||||
logger: testLogger(t).WithFields(),
|
||||
}
|
||||
dtos := []dto.TaskDTO{{ID: "task-1"}}
|
||||
h.enrichTasksWithPRs(context.Background(), dtos)
|
||||
assert.Nil(t, dtos[0].PRs, "PRs left empty when the lister errors")
|
||||
}
|
||||
@@ -255,7 +255,7 @@ func (s *Server) registerConfigExecutorTools() {
|
||||
func (s *Server) registerConfigTaskTools() {
|
||||
s.mcpServer.AddTool(
|
||||
mcp.NewTool("list_tasks_kandev",
|
||||
mcp.WithDescription("List all tasks in a workflow."),
|
||||
mcp.WithDescription("List all tasks in a workflow. Each task includes its associated GitHub pull requests (number, url, title, state) under the \"prs\" field when any exist — use the PR state (open/closed/merged) to find tasks whose work has landed."),
|
||||
mcp.WithString("workflow_id", mcp.Required(), mcp.Description("The workflow ID")),
|
||||
),
|
||||
s.wrapHandler("list_tasks_kandev", s.listTasksHandler()),
|
||||
|
||||
@@ -18,8 +18,9 @@ func (s *Server) registerRelatedTasksTool() {
|
||||
mcp.NewTool("list_related_tasks_kandev",
|
||||
mcp.WithDescription(
|
||||
`List parent, children, siblings, blockers, and blocked tasks for the current task.
|
||||
Use this to discover task IDs you can reach via message_task_kandev. In office mode, document keys are
|
||||
also included so you can fetch documents with get_task_document_kandev.
|
||||
Use this to discover task IDs you can reach via message_task_kandev. Each related task includes its
|
||||
associated GitHub pull requests (number, url, title, state) under the "prs" field when any exist. In
|
||||
office mode, document keys are also included so you can fetch documents with get_task_document_kandev.
|
||||
Pass task_id to inspect a different task in the same workspace.`,
|
||||
),
|
||||
mcp.WithString("task_id", mcp.Description("Defaults to the current task.")),
|
||||
|
||||
@@ -392,7 +392,7 @@ func (s *Server) registerKanbanTools() {
|
||||
)
|
||||
s.mcpServer.AddTool(
|
||||
mcp.NewTool("list_tasks_kandev",
|
||||
mcp.WithDescription("List all tasks in a workflow."),
|
||||
mcp.WithDescription("List all tasks in a workflow. Each task includes its associated GitHub pull requests (number, url, title, state) under the \"prs\" field when any exist — use the PR state (open/closed/merged) to find tasks whose work has landed."),
|
||||
mcp.WithString("workflow_id", mcp.Required(), mcp.Description("The workflow ID")),
|
||||
),
|
||||
s.wrapHandler("list_tasks_kandev", s.listTasksHandler()),
|
||||
|
||||
@@ -157,6 +157,12 @@ type TaskDTO struct {
|
||||
// kanban-board tasks. Use this to gate office-only UI (e.g. the
|
||||
// "Open in office view" topbar link).
|
||||
IsFromOffice bool `json:"is_from_office,omitempty"`
|
||||
|
||||
// PRs lists the GitHub pull requests associated with this task, when the
|
||||
// github service is wired and any association exists. Surfaced through the
|
||||
// task-listing MCP tools so agents can reason about PR status (e.g. find
|
||||
// tasks whose PRs are merged). Omitted when empty.
|
||||
PRs []v1.TaskPRSummary `json:"prs,omitempty"`
|
||||
}
|
||||
|
||||
type TaskRepositoryDTO struct {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
orchmodels "github.com/kandev/kandev/internal/office/models"
|
||||
"github.com/kandev/kandev/internal/task/models"
|
||||
"github.com/kandev/kandev/internal/task/repository"
|
||||
v1 "github.com/kandev/kandev/pkg/api/v1"
|
||||
)
|
||||
|
||||
// Workspace-mode and ordering constants used across handoff plumbing.
|
||||
@@ -137,14 +138,15 @@ func (p WorkspacePolicy) NeedsAttachment() bool {
|
||||
// list_related_tasks_kandev MCP tool. Document keys are precomputed so an
|
||||
// agent can decide what to fetch without a follow-up list call.
|
||||
type RelatedTask struct {
|
||||
ID string `json:"id"`
|
||||
Identifier string `json:"identifier,omitempty"`
|
||||
Title string `json:"title"`
|
||||
State string `json:"state"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
AssigneeLabel string `json:"assignee_label,omitempty"`
|
||||
DocumentKeys []string `json:"document_keys,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Identifier string `json:"identifier,omitempty"`
|
||||
Title string `json:"title"`
|
||||
State string `json:"state"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
AssigneeLabel string `json:"assignee_label,omitempty"`
|
||||
DocumentKeys []string `json:"document_keys,omitempty"`
|
||||
PRs []v1.TaskPRSummary `json:"prs,omitempty"`
|
||||
}
|
||||
|
||||
// RelatedTasks bundles every relation surface for a single task.
|
||||
|
||||
@@ -18,6 +18,18 @@ const (
|
||||
TaskStateCancelled TaskState = "CANCELLED"
|
||||
)
|
||||
|
||||
// TaskPRSummary is a compact view of a GitHub pull request associated with a
|
||||
// task. Surfaced through the task-listing MCP tools so agents can reason about
|
||||
// PR status. State is one of "open", "closed", "merged"; MergedAt is set only
|
||||
// when the PR has merged, so agents can report when the work landed.
|
||||
type TaskPRSummary struct {
|
||||
Number int `json:"number"`
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title,omitempty"`
|
||||
State string `json:"state"`
|
||||
MergedAt *time.Time `json:"merged_at,omitempty"`
|
||||
}
|
||||
|
||||
// TaskSessionState represents the state of an agent session.
|
||||
type TaskSessionState string
|
||||
|
||||
|
||||
Reference in New Issue
Block a user