diff --git a/actions/setup/js/create_issue.cjs b/actions/setup/js/create_issue.cjs index 385579ca33e..7daf568e257 100644 --- a/actions/setup/js/create_issue.cjs +++ b/actions/setup/js/create_issue.cjs @@ -778,7 +778,13 @@ async function main(config = {}) { const bodyLines = processedBody.split("\n"); if (!title) { - title = message.body ?? "Agent Output"; + // Use the first non-empty line of the body as the title fallback rather than + // the entire body, so the title stays concise and the body remains intact. + const firstBodyLine = (message.body ?? "") + .split("\n") + .map(l => l.replace(/^#+\s*/, "").trim()) + .find(l => l.length > 0); + title = firstBodyLine || "Agent Output"; } // Sanitize title for Unicode security and remove any duplicate prefixes diff --git a/actions/setup/js/create_issue.test.cjs b/actions/setup/js/create_issue.test.cjs index 3d20ea4214a..236b7c7407b 100644 --- a/actions/setup/js/create_issue.test.cjs +++ b/actions/setup/js/create_issue.test.cjs @@ -138,6 +138,20 @@ describe("create_issue", () => { ); }); + it("should use the first meaningful body line as title when title is missing", async () => { + const handler = await main({}); + const body = "\n\n## Incident Summary\n\nFull details line 1\nFull details line 2"; + const result = await handler({ body }); + + expect(result.success).toBe(true); + expect(mockGithub.rest.issues.create).toHaveBeenCalledWith( + expect.objectContaining({ + title: "Incident Summary", + body: expect.stringContaining("Full details line 1\nFull details line 2"), + }) + ); + }); + it("should use 'Agent Output' as title when both title and body are missing", async () => { const handler = await main({}); const result = await handler({}); diff --git a/actions/setup/js/intent_probe.cjs b/actions/setup/js/intent_probe.cjs index fdef70a6949..a3c36faaeea 100644 --- a/actions/setup/js/intent_probe.cjs +++ b/actions/setup/js/intent_probe.cjs @@ -38,7 +38,12 @@ function resolveIssueTitleForValidation(entry) { return entry.title; } if (typeof entry.body === "string" && entry.body.trim()) { - return entry.body; + // Mirror create_issue's fallback: use the first non-empty line of the body. + const firstBodyLine = entry.body + .split("\n") + .map(l => l.replace(/^#+\s*/, "").trim()) + .find(l => l.length > 0); + if (firstBodyLine) return firstBodyLine; } // Mirror create_issue's own fallback so probe validation sees the same // effective title the handler would ultimately use when both fields are absent. diff --git a/actions/setup/js/mcp_scripts_mcp_server_http.test.cjs b/actions/setup/js/mcp_scripts_mcp_server_http.test.cjs index 7e8340fbcf5..5b9e9a171e7 100644 --- a/actions/setup/js/mcp_scripts_mcp_server_http.test.cjs +++ b/actions/setup/js/mcp_scripts_mcp_server_http.test.cjs @@ -40,7 +40,12 @@ describe("mcp_scripts_mcp_server_http.cjs integration", () => { serverName: "http-integration-test-server", version: "1.0.0", tools: [ - { name: "echo_tool", description: "Echoes the input message", inputSchema: { type: "object", properties: { message: { type: "string", description: "Message to echo" } }, required: ["message"] }, handler: "echo-handler.cjs" }, + { + name: "echo_tool", + description: "Echoes the input message", + inputSchema: { type: "object", properties: { message: { type: "string", description: "Message to echo", minLength: 20 } }, required: ["message"] }, + handler: "echo-handler.cjs", + }, ], }) ), @@ -146,6 +151,15 @@ describe("mcp_scripts_mcp_server_http.cjs integration", () => { response = await makeRequest({ jsonrpc: "2.0", id: 4, method: "tools/call", params: { name: "echo_tool", arguments: {} } }, headers); (expect(response.status).toBe(200), expect(response.data.error).toBeDefined(), expect(response.data.error.message).toContain("not allowed")); }), + it("should enforce minLength from tool schema on HTTP calls", async () => { + const headers = sessionId ? { "Mcp-Session-Id": sessionId } : {}, + response = await makeRequest({ jsonrpc: "2.0", id: 5, method: "tools/call", params: { name: "echo_tool", arguments: { message: "too short" } } }, headers); + (expect(response.status).toBe(200), + expect(response.data.error).toBeDefined(), + expect(response.data.error.code).toBe(-32602), + expect(response.data.error.message).toContain("Invalid arguments"), + expect(response.data.error.message).toContain("minimum 20 characters")); + }), afterAll(async () => { (serverProcess && (serverProcess.kill("SIGTERM"), diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index eac9c107b6b..021cbf8fe9d 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -1697,7 +1697,13 @@ function createHandlers(server, appendSafeOutput, config = {}) { let resolvedTitle = entry.title?.trim() || ""; if (!resolvedTitle) { - resolvedTitle = entry.body?.trim() || "Agent Output"; + // Use the first non-empty line of the body as the title fallback rather than + // the entire body, so the title stays concise and the body remains intact. + const firstBodyLine = (entry.body || "") + .split("\n") + .map(l => l.replace(/^#+\s*/, "").trim()) + .find(l => l.length > 0); + resolvedTitle = firstBodyLine || "Agent Output"; } resolvedTitle = applyTitlePrefix(sanitizeTitle(resolvedTitle, createIssueTitlePrefix), createIssueTitlePrefix); diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index a30d63a6ab5..e1c94c28134 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -105,6 +105,8 @@ describe("safe_outputs_handlers", () => { it("resolves issue title using the create_issue fallback order", () => { expect(resolveIssueTitleForValidation({ title: "Real title", body: "Ignored body" })).toBe("Real title"); expect(resolveIssueTitleForValidation({ body: "Body title" })).toBe("Body title"); + expect(resolveIssueTitleForValidation({ body: "\n\n## Incident Summary\n\nBody details" })).toBe("Incident Summary"); + expect(resolveIssueTitleForValidation({ body: " \n\n " })).toBe("Agent Output"); expect(resolveIssueTitleForValidation({})).toBe("Agent Output"); }); @@ -2305,6 +2307,40 @@ describe("safe_outputs_handlers", () => { }); describe("createIssueHandler", () => { + it("should deduplicate against fallback title derived from first meaningful body line", () => { + const h = createHandlers(mockServer, mockAppendSafeOutput, { + create_issue: { + deduplicate_by_title: true, + }, + }); + + const first = h.createIssueHandler({ body: "\n\n## Incident Summary\n\nBody A details" }); + const second = h.createIssueHandler({ title: "Incident Summary", body: "Body B details" }); + + const firstResponse = JSON.parse(first.content[0].text); + const secondResponse = JSON.parse(second.content[0].text); + expect(firstResponse.result).toBe("success"); + expect(secondResponse.result).toBe("duplicate_dropped"); + const droppedEntry = mockAppendSafeOutput.mock.calls[1][0]; + expect(droppedEntry._dropped_duplicate_by_title).toBe(true); + }); + + it("should fall back to Agent Output when title and body are blank", () => { + const h = createHandlers(mockServer, mockAppendSafeOutput, { + create_issue: { + deduplicate_by_title: true, + }, + }); + + const first = h.createIssueHandler({ body: " \n\n " }); + const second = h.createIssueHandler({ title: "Agent Output", body: "Real details for duplicate check" }); + + const firstResponse = JSON.parse(first.content[0].text); + const secondResponse = JSON.parse(second.content[0].text); + expect(firstResponse.result).toBe("success"); + expect(secondResponse.result).toBe("duplicate_dropped"); + }); + it("should append create_issue entry when dedup is disabled", () => { handlers.createIssueHandler({ title: "Issue A", body: "Body A" }); handlers.createIssueHandler({ title: "Issue A", body: "Body A again" }); diff --git a/actions/setup/md/safe_outputs_auto_create_issue.md b/actions/setup/md/safe_outputs_auto_create_issue.md index cd28d964e14..448a12f52ef 100644 --- a/actions/setup/md/safe_outputs_auto_create_issue.md +++ b/actions/setup/md/safe_outputs_auto_create_issue.md @@ -1,2 +1,8 @@ **IMPORTANT**: Report your findings or results by creating a GitHub issue using the create_issue tool. If you have no meaningful results to report, call the noop tool instead. + +When calling create_issue: +- `title`: a short, descriptive summary (aim for one sentence, ≤ 100 characters) +- `body`: the full content — details, findings, context, and any supporting information + +Do **not** put all the content in the title; the title is a headline, and the body is where the substance goes.