From 8d786bd8b957e2c08ad777ca076187da8f9d6bf4 Mon Sep 17 00:00:00 2001 From: Boaz Reicher <44614829+boazreicher@users.noreply.github.com> Date: Wed, 27 May 2026 12:01:48 +0000 Subject: [PATCH 1/3] adding suggestions for labels and fields --- docs/feature-flags.md | 1 + .../__toolsnaps__/set_issue_fields.snap | 4 + .../__toolsnaps__/update_issue_labels.snap | 4 + pkg/github/issues_granular.go | 90 +++++++++++++++++-- 4 files changed, 91 insertions(+), 8 deletions(-) diff --git a/docs/feature-flags.md b/docs/feature-flags.md index a552e71a04..f291f57f31 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -177,6 +177,7 @@ runtime behavior (such as output formatting) won't appear here. - **update_issue_type** - Update Issue Type - **Required OAuth Scopes**: `repo` + - `is_suggestion`: If true, propose the issue type change instead of applying it. Defaults to false, which applies the change to the issue. (boolean, optional) - `issue_number`: The issue number to update (number, required) - `issue_type`: The issue type to set (string, required) - `owner`: Repository owner (username or organization) (string, required) diff --git a/pkg/github/__toolsnaps__/set_issue_fields.snap b/pkg/github/__toolsnaps__/set_issue_fields.snap index 979dde4fb3..bff3af0902 100644 --- a/pkg/github/__toolsnaps__/set_issue_fields.snap +++ b/pkg/github/__toolsnaps__/set_issue_fields.snap @@ -23,6 +23,10 @@ "description": "The GraphQL node ID of the issue field", "type": "string" }, + "is_suggestion": { + "description": "If true, propose this field value instead of applying it. Fields not marked as suggestions are applied to the issue; suggested fields are returned as proposals in the response.", + "type": "boolean" + }, "number_value": { "description": "The value to set for a number field", "type": "number" diff --git a/pkg/github/__toolsnaps__/update_issue_labels.snap b/pkg/github/__toolsnaps__/update_issue_labels.snap index 89ff86b2ff..bccf034ec3 100644 --- a/pkg/github/__toolsnaps__/update_issue_labels.snap +++ b/pkg/github/__toolsnaps__/update_issue_labels.snap @@ -22,6 +22,10 @@ }, { "properties": { + "is_suggestion": { + "description": "If true, propose this label instead of applying it. Labels not marked as suggestions are applied to the issue; suggested labels are returned as proposals in the response.", + "type": "boolean" + }, "name": { "description": "Label name", "type": "string" diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go index 9e789c6d16..037740ca1a 100644 --- a/pkg/github/issues_granular.go +++ b/pkg/github/issues_granular.go @@ -320,6 +320,11 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S "State the concrete signal (e.g. 'Reports a crash when saving' → bug).", MaxLength: jsonschema.Ptr(280), }, + "is_suggestion": { + Type: "boolean", + Description: "If true, propose this label instead of applying it. " + + "Labels not marked as suggestions are applied to the issue; suggested labels are returned as proposals in the response.", + }, }, Required: []string{"name"}, }, @@ -364,6 +369,7 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S anyRationale := false payload := make([]any, 0, len(labelsSlice)) + suggested := make([]any, 0) for _, item := range labelsSlice { switch v := item.(type) { case string: @@ -381,6 +387,14 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S if len([]rune(rationale)) > 280 { return utils.NewToolResultError("label rationale must be 280 characters or less"), nil, nil } + isSuggestion, err := OptionalParam[bool](v, "is_suggestion") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + if isSuggestion { + suggested = append(suggested, labelWithRationale{Name: name, Rationale: rationale}) + continue + } if rationale == "" { payload = append(payload, name) } else { @@ -392,6 +406,21 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S } } + // If no labels are to be applied, only return suggestions without + // calling the API. + if len(payload) == 0 { + r, err := json.Marshal(map[string]any{ + "owner": owner, + "repo": repo, + "issue_number": issueNumber, + "suggested_labels": suggested, + }) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to marshal suggestion", err), nil, nil + } + return utils.NewToolResultText(string(r)), nil, nil + } + client, err := deps.GetClient(ctx) if err != nil { return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil @@ -422,10 +451,14 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S } defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(MinimalResponse{ - ID: fmt.Sprintf("%d", issue.GetID()), - URL: issue.GetHTMLURL(), - }) + respBody := map[string]any{ + "id": fmt.Sprintf("%d", issue.GetID()), + "url": issue.GetHTMLURL(), + } + if len(suggested) > 0 { + respBody["suggested_labels"] = suggested + } + r, err := json.Marshal(respBody) if err != nil { return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } @@ -961,6 +994,11 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv "State the concrete signal (e.g. 'Reports a crash when saving' → high priority).", MaxLength: jsonschema.Ptr(280), }, + "is_suggestion": { + Type: "boolean", + Description: "If true, propose this field value instead of applying it. " + + "Fields not marked as suggestions are applied to the issue; suggested fields are returned as proposals in the response.", + }, }, Required: []string{"field_id"}, }, @@ -1010,6 +1048,7 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv } issueFields := make([]IssueFieldCreateOrUpdateInput, 0, len(fieldMaps)) + suggestedFields := make([]map[string]any, 0) for _, fieldMap := range fieldMaps { fieldID, err := RequiredParam[string](fieldMap, "field_id") if err != nil { @@ -1019,28 +1058,33 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv input := IssueFieldCreateOrUpdateInput{ FieldID: githubv4.ID(fieldID), } + display := map[string]any{"field_id": fieldID} // Count how many value keys are present; exactly one is required. valueCount := 0 if v, err := OptionalParam[string](fieldMap, "text_value"); err == nil && v != "" { input.TextValue = githubv4.NewString(githubv4.String(v)) + display["text_value"] = v valueCount++ } if v, err := OptionalParam[float64](fieldMap, "number_value"); err == nil { if _, exists := fieldMap["number_value"]; exists { gqlFloat := githubv4.Float(v) input.NumberValue = &gqlFloat + display["number_value"] = v valueCount++ } } if v, err := OptionalParam[string](fieldMap, "date_value"); err == nil && v != "" { input.DateValue = githubv4.NewString(githubv4.String(v)) + display["date_value"] = v valueCount++ } if v, err := OptionalParam[string](fieldMap, "single_select_option_id"); err == nil && v != "" { optionID := githubv4.ID(v) input.SingleSelectOptionID = &optionID + display["single_select_option_id"] = v valueCount++ } if _, exists := fieldMap["delete"]; exists { @@ -1048,6 +1092,7 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv if err == nil && del { deleteVal := githubv4.Boolean(true) input.Delete = &deleteVal + display["delete"] = true valueCount++ } } @@ -1070,12 +1115,37 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv } if rationale != "" { input.Rationale = githubv4.NewString(githubv4.String(rationale)) + display["rationale"] = rationale } } + isSuggestion, err := OptionalParam[bool](fieldMap, "is_suggestion") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + if isSuggestion { + suggestedFields = append(suggestedFields, display) + continue + } + issueFields = append(issueFields, input) } + // If no fields are to be applied, only return suggestions without + // calling the API. + if len(issueFields) == 0 { + r, err := json.Marshal(map[string]any{ + "owner": owner, + "repo": repo, + "issue_number": issueNumber, + "suggested_fields": suggestedFields, + }) + if err != nil { + return utils.NewToolResultErrorFromErr("failed to marshal suggestion", err), nil, nil + } + return utils.NewToolResultText(string(r)), nil, nil + } + gqlClient, err := deps.GetGQLClient(ctx) if err != nil { return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil @@ -1121,10 +1191,14 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to set issue field values", err), nil, nil } - r, err := json.Marshal(MinimalResponse{ - ID: fmt.Sprintf("%v", mutation.SetIssueFieldValue.Issue.ID), - URL: string(mutation.SetIssueFieldValue.Issue.URL), - }) + respBody := map[string]any{ + "id": fmt.Sprintf("%v", mutation.SetIssueFieldValue.Issue.ID), + "url": string(mutation.SetIssueFieldValue.Issue.URL), + } + if len(suggestedFields) > 0 { + respBody["suggested_fields"] = suggestedFields + } + r, err := json.Marshal(respBody) if err != nil { return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } From 592af2c15dcb34a990a572919580451ddc379e32 Mon Sep 17 00:00:00 2001 From: Boaz Reicher <44614829+boazreicher@users.noreply.github.com> Date: Wed, 27 May 2026 12:25:26 +0000 Subject: [PATCH 2/3] fixing suggestions --- docs/feature-flags.md | 2 +- .../__toolsnaps__/set_issue_fields.snap | 2 +- .../__toolsnaps__/update_issue_labels.snap | 2 +- .../__toolsnaps__/update_issue_type.snap | 2 +- pkg/github/granular_tools_test.go | 63 ++++----- pkg/github/issues_granular.go | 127 +++++------------- 6 files changed, 64 insertions(+), 134 deletions(-) diff --git a/docs/feature-flags.md b/docs/feature-flags.md index f291f57f31..284ab2edfa 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -177,7 +177,7 @@ runtime behavior (such as output formatting) won't appear here. - **update_issue_type** - Update Issue Type - **Required OAuth Scopes**: `repo` - - `is_suggestion`: If true, propose the issue type change instead of applying it. Defaults to false, which applies the change to the issue. (boolean, optional) + - `is_suggestion`: If true, this issue type change is sent to the API as a suggestion (suggest:true) rather than an applied value. Whether the type is applied or recorded as a proposal is determined by the API. (boolean, optional) - `issue_number`: The issue number to update (number, required) - `issue_type`: The issue type to set (string, required) - `owner`: Repository owner (username or organization) (string, required) diff --git a/pkg/github/__toolsnaps__/set_issue_fields.snap b/pkg/github/__toolsnaps__/set_issue_fields.snap index bff3af0902..88c88fdc65 100644 --- a/pkg/github/__toolsnaps__/set_issue_fields.snap +++ b/pkg/github/__toolsnaps__/set_issue_fields.snap @@ -24,7 +24,7 @@ "type": "string" }, "is_suggestion": { - "description": "If true, propose this field value instead of applying it. Fields not marked as suggestions are applied to the issue; suggested fields are returned as proposals in the response.", + "description": "If true, this field value is sent to the API as a suggestion (suggest:true) rather than an applied value. Whether the value is applied or recorded as a proposal is determined by the API.", "type": "boolean" }, "number_value": { diff --git a/pkg/github/__toolsnaps__/update_issue_labels.snap b/pkg/github/__toolsnaps__/update_issue_labels.snap index bccf034ec3..3bdbdfc9ef 100644 --- a/pkg/github/__toolsnaps__/update_issue_labels.snap +++ b/pkg/github/__toolsnaps__/update_issue_labels.snap @@ -23,7 +23,7 @@ { "properties": { "is_suggestion": { - "description": "If true, propose this label instead of applying it. Labels not marked as suggestions are applied to the issue; suggested labels are returned as proposals in the response.", + "description": "If true, this label is sent to the API as a suggestion (suggest:true) rather than an applied label. Whether the label is applied or recorded as a proposal is determined by the API.", "type": "boolean" }, "name": { diff --git a/pkg/github/__toolsnaps__/update_issue_type.snap b/pkg/github/__toolsnaps__/update_issue_type.snap index 7fb5fde894..da749cd466 100644 --- a/pkg/github/__toolsnaps__/update_issue_type.snap +++ b/pkg/github/__toolsnaps__/update_issue_type.snap @@ -8,7 +8,7 @@ "inputSchema": { "properties": { "is_suggestion": { - "description": "If true, propose the issue type change instead of applying it. Defaults to false, which applies the change to the issue.", + "description": "If true, this issue type change is sent to the API as a suggestion (suggest:true) rather than an applied value. Whether the type is applied or recorded as a proposal is determined by the API.", "type": "boolean" }, "issue_number": { diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go index 88bd560b4f..ff025c65c6 100644 --- a/pkg/github/granular_tools_test.go +++ b/pkg/github/granular_tools_test.go @@ -2,7 +2,6 @@ package github import ( "context" - "encoding/json" "net/http" "strings" "testing" @@ -463,62 +462,58 @@ func TestGranularUpdateIssueTypeSuggest(t *testing.T) { tests := []struct { name string requestArgs map[string]any - expected map[string]any + expectedReq map[string]any }{ { name: "suggest without rationale", requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "issue_number": float64(1), - "issue_type": "bug", - "suggest": true, + "owner": "owner", + "repo": "repo", + "issue_number": float64(1), + "issue_type": "bug", + "is_suggestion": true, }, - expected: map[string]any{ - "owner": "owner", - "repo": "repo", - "issue_number": float64(1), - "issue_type": "bug", - "suggested": true, + expectedReq: map[string]any{ + "type": map[string]any{ + "value": "bug", + "suggest": true, + }, }, }, { name: "suggest with rationale", requestArgs: map[string]any{ - "owner": "owner", - "repo": "repo", - "issue_number": float64(1), - "issue_type": "feature", - "rationale": " Asks for dark mode support ", - "suggest": true, + "owner": "owner", + "repo": "repo", + "issue_number": float64(1), + "issue_type": "feature", + "rationale": " Asks for dark mode support ", + "is_suggestion": true, }, - expected: map[string]any{ - "owner": "owner", - "repo": "repo", - "issue_number": float64(1), - "issue_type": "feature", - "rationale": "Asks for dark mode support", - "suggested": true, + expectedReq: map[string]any{ + "type": map[string]any{ + "value": "feature", + "rationale": "Asks for dark mode support", + "suggest": true, + }, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - // No HTTP handler registered: any API call would fail the test. - deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))} + client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq). + andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})), + })) + deps := BaseDeps{Client: client} serverTool := GranularUpdateIssueType(translations.NullTranslationHelper) handler := serverTool.Handler(deps) request := createMCPRequest(tc.requestArgs) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) - require.False(t, result.IsError) - - textContent := getTextResult(t, result) - var got map[string]any - require.NoError(t, json.Unmarshal([]byte(textContent.Text), &got)) - assert.Equal(t, tc.expected, got) + assert.False(t, result.IsError) }) } } diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go index 037740ca1a..67e9103309 100644 --- a/pkg/github/issues_granular.go +++ b/pkg/github/issues_granular.go @@ -259,10 +259,11 @@ func GranularUpdateIssueAssignees(t translations.TranslationHelperFunc) inventor } // labelWithRationale represents the object form of a label entry, allowing a -// rationale to be sent alongside the label name. +// rationale and/or suggest flag to be sent alongside the label name. type labelWithRationale struct { Name string `json:"name"` Rationale string `json:"rationale,omitempty"` + Suggest bool `json:"suggest,omitempty"` } // labelsUpdateRequest is a custom request body for updating an issue's labels @@ -322,8 +323,8 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S }, "is_suggestion": { Type: "boolean", - Description: "If true, propose this label instead of applying it. " + - "Labels not marked as suggestions are applied to the issue; suggested labels are returned as proposals in the response.", + Description: "If true, this label is sent to the API as a suggestion (suggest:true) rather than an applied label. " + + "Whether the label is applied or recorded as a proposal is determined by the API.", }, }, Required: []string{"name"}, @@ -367,9 +368,8 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S } } - anyRationale := false + useObjectForm := false payload := make([]any, 0, len(labelsSlice)) - suggested := make([]any, 0) for _, item := range labelsSlice { switch v := item.(type) { case string: @@ -391,46 +391,27 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } - if isSuggestion { - suggested = append(suggested, labelWithRationale{Name: name, Rationale: rationale}) - continue - } - if rationale == "" { + if rationale == "" && !isSuggestion { payload = append(payload, name) } else { - anyRationale = true - payload = append(payload, labelWithRationale{Name: name, Rationale: rationale}) + useObjectForm = true + payload = append(payload, labelWithRationale{Name: name, Rationale: rationale, Suggest: isSuggestion}) } default: return utils.NewToolResultError("each label must be a string or an object with 'name' and optional 'rationale'"), nil, nil } } - // If no labels are to be applied, only return suggestions without - // calling the API. - if len(payload) == 0 { - r, err := json.Marshal(map[string]any{ - "owner": owner, - "repo": repo, - "issue_number": issueNumber, - "suggested_labels": suggested, - }) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to marshal suggestion", err), nil, nil - } - return utils.NewToolResultText(string(r)), nil, nil - } - client, err := deps.GetClient(ctx) if err != nil { return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } var body any - if anyRationale { + if useObjectForm { body = &labelsUpdateRequest{Labels: payload} } else { - // Preserve the standard wire format when no rationale is supplied. + // Preserve the standard wire format when no rationale or suggest is supplied. names := make([]string, len(payload)) for i, p := range payload { names[i] = p.(string) @@ -451,14 +432,10 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S } defer func() { _ = resp.Body.Close() }() - respBody := map[string]any{ - "id": fmt.Sprintf("%d", issue.GetID()), - "url": issue.GetHTMLURL(), - } - if len(suggested) > 0 { - respBody["suggested_labels"] = suggested - } - r, err := json.Marshal(respBody) + r, err := json.Marshal(MinimalResponse{ + ID: fmt.Sprintf("%d", issue.GetID()), + URL: issue.GetHTMLURL(), + }) if err != nil { return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } @@ -494,10 +471,11 @@ func GranularUpdateIssueMilestone(t translations.TranslationHelperFunc) inventor } // issueTypeWithRationale represents the object form of the issue type field, -// allowing a rationale to be sent alongside the type name. +// allowing a rationale and/or suggest flag to be sent alongside the type name. type issueTypeWithRationale struct { Value string `json:"value"` - Rationale string `json:"rationale"` + Rationale string `json:"rationale,omitempty"` + Suggest bool `json:"suggest,omitempty"` } // issueTypeUpdateRequest is a custom request body for updating an issue type @@ -547,8 +525,8 @@ func GranularUpdateIssueType(t translations.TranslationHelperFunc) inventory.Ser }, "is_suggestion": { Type: "boolean", - Description: "If true, propose the issue type change instead of applying it. " + - "Defaults to false, which applies the change to the issue.", + Description: "If true, this issue type change is sent to the API as a suggestion (suggest:true) rather than an applied value. " + + "Whether the type is applied or recorded as a proposal is determined by the API.", }, }, Required: []string{"owner", "repo", "issue_number", "issue_type"}, @@ -580,40 +558,23 @@ func GranularUpdateIssueType(t translations.TranslationHelperFunc) inventory.Ser if len([]rune(rationale)) > 280 { return utils.NewToolResultError("parameter rationale must be 280 characters or less"), nil, nil } - suggest, err := OptionalParam[bool](args, "suggest") + isSuggestion, err := OptionalParam[bool](args, "is_suggestion") if err != nil { return utils.NewToolResultError(err.Error()), nil, nil } - if suggest { - suggestion := map[string]any{ - "owner": owner, - "repo": repo, - "issue_number": issueNumber, - "issue_type": issueType, - "suggested": true, - } - if rationale != "" { - suggestion["rationale"] = rationale - } - r, err := json.Marshal(suggestion) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to marshal suggestion", err), nil, nil - } - return utils.NewToolResultText(string(r)), nil, nil - } - client, err := deps.GetClient(ctx) if err != nil { return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } var body any - if rationale != "" { + if rationale != "" || isSuggestion { body = &issueTypeUpdateRequest{ Type: issueTypeWithRationale{ Value: issueType, Rationale: rationale, + Suggest: isSuggestion, }, } } else { @@ -926,6 +887,7 @@ type IssueFieldCreateOrUpdateInput struct { SingleSelectOptionID *githubv4.ID `json:"singleSelectOptionId,omitempty"` Delete *githubv4.Boolean `json:"delete,omitempty"` Rationale *githubv4.String `json:"rationale,omitempty"` + Suggest *githubv4.Boolean `json:"suggest,omitempty"` } // GranularSetIssueFields creates a tool to set issue field values on an issue using GraphQL. @@ -996,8 +958,8 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv }, "is_suggestion": { Type: "boolean", - Description: "If true, propose this field value instead of applying it. " + - "Fields not marked as suggestions are applied to the issue; suggested fields are returned as proposals in the response.", + Description: "If true, this field value is sent to the API as a suggestion (suggest:true) rather than an applied value. " + + "Whether the value is applied or recorded as a proposal is determined by the API.", }, }, Required: []string{"field_id"}, @@ -1048,7 +1010,6 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv } issueFields := make([]IssueFieldCreateOrUpdateInput, 0, len(fieldMaps)) - suggestedFields := make([]map[string]any, 0) for _, fieldMap := range fieldMaps { fieldID, err := RequiredParam[string](fieldMap, "field_id") if err != nil { @@ -1058,33 +1019,28 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv input := IssueFieldCreateOrUpdateInput{ FieldID: githubv4.ID(fieldID), } - display := map[string]any{"field_id": fieldID} // Count how many value keys are present; exactly one is required. valueCount := 0 if v, err := OptionalParam[string](fieldMap, "text_value"); err == nil && v != "" { input.TextValue = githubv4.NewString(githubv4.String(v)) - display["text_value"] = v valueCount++ } if v, err := OptionalParam[float64](fieldMap, "number_value"); err == nil { if _, exists := fieldMap["number_value"]; exists { gqlFloat := githubv4.Float(v) input.NumberValue = &gqlFloat - display["number_value"] = v valueCount++ } } if v, err := OptionalParam[string](fieldMap, "date_value"); err == nil && v != "" { input.DateValue = githubv4.NewString(githubv4.String(v)) - display["date_value"] = v valueCount++ } if v, err := OptionalParam[string](fieldMap, "single_select_option_id"); err == nil && v != "" { optionID := githubv4.ID(v) input.SingleSelectOptionID = &optionID - display["single_select_option_id"] = v valueCount++ } if _, exists := fieldMap["delete"]; exists { @@ -1092,7 +1048,6 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv if err == nil && del { deleteVal := githubv4.Boolean(true) input.Delete = &deleteVal - display["delete"] = true valueCount++ } } @@ -1115,7 +1070,6 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv } if rationale != "" { input.Rationale = githubv4.NewString(githubv4.String(rationale)) - display["rationale"] = rationale } } @@ -1124,28 +1078,13 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv return utils.NewToolResultError(err.Error()), nil, nil } if isSuggestion { - suggestedFields = append(suggestedFields, display) - continue + suggestVal := githubv4.Boolean(true) + input.Suggest = &suggestVal } issueFields = append(issueFields, input) } - // If no fields are to be applied, only return suggestions without - // calling the API. - if len(issueFields) == 0 { - r, err := json.Marshal(map[string]any{ - "owner": owner, - "repo": repo, - "issue_number": issueNumber, - "suggested_fields": suggestedFields, - }) - if err != nil { - return utils.NewToolResultErrorFromErr("failed to marshal suggestion", err), nil, nil - } - return utils.NewToolResultText(string(r)), nil, nil - } - gqlClient, err := deps.GetGQLClient(ctx) if err != nil { return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil @@ -1191,14 +1130,10 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to set issue field values", err), nil, nil } - respBody := map[string]any{ - "id": fmt.Sprintf("%v", mutation.SetIssueFieldValue.Issue.ID), - "url": string(mutation.SetIssueFieldValue.Issue.URL), - } - if len(suggestedFields) > 0 { - respBody["suggested_fields"] = suggestedFields - } - r, err := json.Marshal(respBody) + r, err := json.Marshal(MinimalResponse{ + ID: fmt.Sprintf("%v", mutation.SetIssueFieldValue.Issue.ID), + URL: string(mutation.SetIssueFieldValue.Issue.URL), + }) if err != nil { return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } From d8c7ac72524306c7b9ce1ef01526b36fd69e2ed4 Mon Sep 17 00:00:00 2001 From: Boaz Reicher <44614829+boazreicher@users.noreply.github.com> Date: Wed, 27 May 2026 06:52:54 -0700 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkg/github/issues_granular.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go index 67e9103309..73fa75413c 100644 --- a/pkg/github/issues_granular.go +++ b/pkg/github/issues_granular.go @@ -398,7 +398,7 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S payload = append(payload, labelWithRationale{Name: name, Rationale: rationale, Suggest: isSuggestion}) } default: - return utils.NewToolResultError("each label must be a string or an object with 'name' and optional 'rationale'"), nil, nil + return utils.NewToolResultError("each label must be a string or an object with 'name' and optional 'rationale' and/or 'is_suggestion'"), nil, nil } }