From 71cd14da928e598eb2096c633afe5682709b8563 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:45:27 +0000 Subject: [PATCH 01/10] Initial plan From d550bf7a4ba8a6a731ad8ebe9295ce6b86164102 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:55:34 +0000 Subject: [PATCH 02/10] Add initialization section to Claude log parser Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/ci-doctor.lock.yml | 121 +++++++++++++++- .../test-ai-inference-github-models.lock.yml | 121 +++++++++++++++- .../test-claude-add-issue-comment.lock.yml | 121 +++++++++++++++- .../test-claude-add-issue-labels.lock.yml | 121 +++++++++++++++- .../workflows/test-claude-command.lock.yml | 121 +++++++++++++++- .../test-claude-create-issue.lock.yml | 121 +++++++++++++++- ...reate-pull-request-review-comment.lock.yml | 121 +++++++++++++++- .../test-claude-create-pull-request.lock.yml | 121 +++++++++++++++- ...eate-repository-security-advisory.lock.yml | 121 +++++++++++++++- pkg/cli/workflows/test-claude-mcp.lock.yml | 121 +++++++++++++++- .../test-claude-push-to-pr-branch.lock.yml | 121 +++++++++++++++- .../test-claude-update-issue.lock.yml | 121 +++++++++++++++- ...playwright-accessibility-contrast.lock.yml | 121 +++++++++++++++- pkg/workflow/js/parse_claude_log.cjs | 135 +++++++++++++++++- pkg/workflow/log_parser_test.go | 68 +++++++++ .../test_data/expected_claude_baseline.md | 27 ++++ 16 files changed, 1789 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index 632ead0e02b..7c2652a74a3 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -1333,7 +1333,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -1470,6 +1480,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -1638,6 +1756,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-ai-inference-github-models.lock.yml b/pkg/cli/workflows/test-ai-inference-github-models.lock.yml index 0399e8e6e57..c58edfaf7af 100644 --- a/pkg/cli/workflows/test-ai-inference-github-models.lock.yml +++ b/pkg/cli/workflows/test-ai-inference-github-models.lock.yml @@ -343,7 +343,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -480,6 +490,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -648,6 +766,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-claude-add-issue-comment.lock.yml b/pkg/cli/workflows/test-claude-add-issue-comment.lock.yml index b16bf6d9dce..d907b84e6dc 100644 --- a/pkg/cli/workflows/test-claude-add-issue-comment.lock.yml +++ b/pkg/cli/workflows/test-claude-add-issue-comment.lock.yml @@ -340,7 +340,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -477,6 +487,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -645,6 +763,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-claude-add-issue-labels.lock.yml b/pkg/cli/workflows/test-claude-add-issue-labels.lock.yml index e0a221349d6..2020a8d7047 100644 --- a/pkg/cli/workflows/test-claude-add-issue-labels.lock.yml +++ b/pkg/cli/workflows/test-claude-add-issue-labels.lock.yml @@ -340,7 +340,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -477,6 +487,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -645,6 +763,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-claude-command.lock.yml b/pkg/cli/workflows/test-claude-command.lock.yml index d9ff0cd94e5..26cf9a8830a 100644 --- a/pkg/cli/workflows/test-claude-command.lock.yml +++ b/pkg/cli/workflows/test-claude-command.lock.yml @@ -340,7 +340,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -477,6 +487,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -645,6 +763,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-claude-create-issue.lock.yml b/pkg/cli/workflows/test-claude-create-issue.lock.yml index 2736c23fd6e..f39eda5c83c 100644 --- a/pkg/cli/workflows/test-claude-create-issue.lock.yml +++ b/pkg/cli/workflows/test-claude-create-issue.lock.yml @@ -340,7 +340,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -477,6 +487,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -645,6 +763,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-claude-create-pull-request-review-comment.lock.yml b/pkg/cli/workflows/test-claude-create-pull-request-review-comment.lock.yml index 21b26a8fb40..95295d8c480 100644 --- a/pkg/cli/workflows/test-claude-create-pull-request-review-comment.lock.yml +++ b/pkg/cli/workflows/test-claude-create-pull-request-review-comment.lock.yml @@ -340,7 +340,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -477,6 +487,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -645,6 +763,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-claude-create-pull-request.lock.yml b/pkg/cli/workflows/test-claude-create-pull-request.lock.yml index 16b613bba25..72086319423 100644 --- a/pkg/cli/workflows/test-claude-create-pull-request.lock.yml +++ b/pkg/cli/workflows/test-claude-create-pull-request.lock.yml @@ -345,7 +345,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -482,6 +492,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -650,6 +768,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-claude-create-repository-security-advisory.lock.yml b/pkg/cli/workflows/test-claude-create-repository-security-advisory.lock.yml index 1d23175a97f..8e98fb6b4b6 100644 --- a/pkg/cli/workflows/test-claude-create-repository-security-advisory.lock.yml +++ b/pkg/cli/workflows/test-claude-create-repository-security-advisory.lock.yml @@ -343,7 +343,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -480,6 +490,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -648,6 +766,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-claude-mcp.lock.yml b/pkg/cli/workflows/test-claude-mcp.lock.yml index 463be71180d..0d2a588b93a 100644 --- a/pkg/cli/workflows/test-claude-mcp.lock.yml +++ b/pkg/cli/workflows/test-claude-mcp.lock.yml @@ -343,7 +343,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -480,6 +490,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -648,6 +766,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-claude-push-to-pr-branch.lock.yml b/pkg/cli/workflows/test-claude-push-to-pr-branch.lock.yml index 4d79c5cc5de..1277f817148 100644 --- a/pkg/cli/workflows/test-claude-push-to-pr-branch.lock.yml +++ b/pkg/cli/workflows/test-claude-push-to-pr-branch.lock.yml @@ -345,7 +345,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -482,6 +492,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -650,6 +768,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-claude-update-issue.lock.yml b/pkg/cli/workflows/test-claude-update-issue.lock.yml index e683f16bc20..097ef99a99e 100644 --- a/pkg/cli/workflows/test-claude-update-issue.lock.yml +++ b/pkg/cli/workflows/test-claude-update-issue.lock.yml @@ -343,7 +343,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -480,6 +490,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -648,6 +766,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml b/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml index da173de5f82..043f0fd4f75 100644 --- a/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml +++ b/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml @@ -1258,7 +1258,17 @@ jobs: if (!Array.isArray(logEntries)) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary // First pass: collect tool results by tool_use_id @@ -1395,6 +1405,114 @@ jobs: return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; } } + /** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ + function formatInitializationSummary(initEntry) { + let markdown = ""; + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + return markdown; + } /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -1563,6 +1681,7 @@ jobs: module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/workflow/js/parse_claude_log.cjs b/pkg/workflow/js/parse_claude_log.cjs index b04e3e60885..77c3215f954 100644 --- a/pkg/workflow/js/parse_claude_log.cjs +++ b/pkg/workflow/js/parse_claude_log.cjs @@ -37,7 +37,20 @@ function parseClaudeLog(logContent) { return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; } - let markdown = "## šŸ¤– Commands and Tools\n\n"; + let markdown = ""; + + // Check for initialization data first + const initEntry = logEntries.find( + entry => entry.type === "system" && entry.subtype === "init" + ); + + if (initEntry) { + markdown += "## šŸš€ Initialization\n\n"; + markdown += formatInitializationSummary(initEntry); + markdown += "\n"; + } + + markdown += "## šŸ¤– Commands and Tools\n\n"; const toolUsePairs = new Map(); // Map tool_use_id to tool_result const commandSummary = []; // For the succinct summary @@ -190,6 +203,125 @@ function parseClaudeLog(logContent) { } } +/** + * Formats initialization information from system init entry + * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. + * @returns {string} Formatted markdown string with initialization summary + */ +function formatInitializationSummary(initEntry) { + let markdown = ""; + + // Display model and session info + if (initEntry.model) { + markdown += `**Model:** ${initEntry.model}\n\n`; + } + + if (initEntry.session_id) { + markdown += `**Session ID:** ${initEntry.session_id}\n\n`; + } + + if (initEntry.cwd) { + // Show a cleaner path by removing common prefixes + const cleanCwd = initEntry.cwd.replace( + /^\/home\/runner\/work\/[^\/]+\/[^\/]+/, + "." + ); + markdown += `**Working Directory:** ${cleanCwd}\n\n`; + } + + // Display MCP servers status + if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { + markdown += "**MCP Servers:**\n"; + for (const server of initEntry.mcp_servers) { + const statusIcon = + server.status === "connected" + ? "āœ…" + : server.status === "failed" + ? "āŒ" + : "ā“"; + markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + } + markdown += "\n"; + } + + // Display tools by category + if (initEntry.tools && Array.isArray(initEntry.tools)) { + markdown += "**Available Tools:**\n"; + + // Categorize tools + const categories = { + Core: [], + "File Operations": [], + "Git/GitHub": [], + MCP: [], + Other: [], + }; + + for (const tool of initEntry.tools) { + if ( + ["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes( + tool + ) + ) { + categories["Core"].push(tool); + } else if ( + [ + "Read", + "Edit", + "MultiEdit", + "Write", + "LS", + "Grep", + "Glob", + "NotebookEdit", + ].includes(tool) + ) { + categories["File Operations"].push(tool); + } else if (tool.startsWith("mcp__github__")) { + categories["Git/GitHub"].push(formatMcpName(tool)); + } else if ( + tool.startsWith("mcp__") || + ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool) + ) { + categories["MCP"].push( + tool.startsWith("mcp__") ? formatMcpName(tool) : tool + ); + } else { + categories["Other"].push(tool); + } + } + + // Display categories with tools + for (const [category, tools] of Object.entries(categories)) { + if (tools.length > 0) { + markdown += `- **${category}:** ${tools.length} tools\n`; + if (tools.length <= 5) { + // Show all tools if 5 or fewer + markdown += ` - ${tools.join(", ")}\n`; + } else { + // Show first few and count + markdown += ` - ${tools.slice(0, 3).join(", ")}, and ${tools.length - 3} more\n`; + } + } + } + markdown += "\n"; + } + + // Display slash commands if available + if (initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { + const commandCount = initEntry.slash_commands.length; + markdown += `**Slash Commands:** ${commandCount} available\n`; + if (commandCount <= 10) { + markdown += `- ${initEntry.slash_commands.join(", ")}\n`; + } else { + markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; + } + markdown += "\n"; + } + + return markdown; +} + /** * Formats a tool use entry with its result into markdown * @param {any} toolUse - The tool use object containing name, input, etc. @@ -384,6 +516,7 @@ if (typeof module !== "undefined" && module.exports) { module.exports = { parseClaudeLog, formatToolUse, + formatInitializationSummary, formatBashCommand, truncateString, }; diff --git a/pkg/workflow/log_parser_test.go b/pkg/workflow/log_parser_test.go index 62fe43d3826..5d9af5c9306 100644 --- a/pkg/workflow/log_parser_test.go +++ b/pkg/workflow/log_parser_test.go @@ -149,3 +149,71 @@ func TestParseClaudeLogSmoke(t *testing.T) { t.Error("Expected error message for empty Claude log") } } + +// Test parsing initialization information from Claude logs +func TestParseClaudeLogInitialization(t *testing.T) { + script := GetLogParserScript("parse_claude_log") + if script == "" { + t.Skip("parse_claude_log script not available") + } + + // Test with initialization log containing system init entry + initClaudeLog := `[ + { + "type": "system", + "subtype": "init", + "cwd": "/home/runner/work/gh-aw/gh-aw", + "session_id": "test-session-123", + "tools": ["Task", "Bash", "Read", "mcp__github__search_issues", "mcp__github__create_issue"], + "mcp_servers": [ + {"name": "github", "status": "connected"}, + {"name": "safe_outputs", "status": "failed"} + ], + "model": "claude-sonnet-4-20250514", + "slash_commands": ["help", "status", "config"] + } +]` + + result, err := runJSLogParser(script, initClaudeLog) + if err != nil { + t.Fatalf("Failed to parse initialization Claude log: %v", err) + } + + // Verify initialization section is present + if !strings.Contains(result, "šŸš€ Initialization") { + t.Error("Expected Claude log output to contain Initialization section") + } + + // Verify model information + if !strings.Contains(result, "claude-sonnet-4-20250514") { + t.Error("Expected Claude log output to contain model information") + } + + // Verify session ID + if !strings.Contains(result, "test-session-123") { + t.Error("Expected Claude log output to contain session ID") + } + + // Verify MCP servers section + if !strings.Contains(result, "MCP Servers") { + t.Error("Expected Claude log output to contain MCP Servers section") + } + + // Verify specific server statuses + if !strings.Contains(result, "āœ… github (connected)") { + t.Error("Expected Claude log output to show github server as connected") + } + if !strings.Contains(result, "āŒ safe_outputs (failed)") { + t.Error("Expected Claude log output to show safe_outputs server as failed") + } + + // Verify tools section + if !strings.Contains(result, "Available Tools") { + t.Error("Expected Claude log output to contain Available Tools section") + } + + // Verify slash commands section + if !strings.Contains(result, "Slash Commands") { + t.Error("Expected Claude log output to contain Slash Commands section") + } +} diff --git a/pkg/workflow/test_data/expected_claude_baseline.md b/pkg/workflow/test_data/expected_claude_baseline.md index a3d794b27f1..4032872d67a 100644 --- a/pkg/workflow/test_data/expected_claude_baseline.md +++ b/pkg/workflow/test_data/expected_claude_baseline.md @@ -1,3 +1,30 @@ +## šŸš€ Initialization + +**Model:** claude-sonnet-4-20250514 + +**Session ID:** a285c0aa-8f9c-4de2-b905-e3d8cf646c4c + +**Working Directory:** . + +**MCP Servers:** +- āœ… github (connected) + +**Available Tools:** +- **Core:** 5 tools + - Task, Bash, ExitPlanMode, BashOutput, KillBash +- **File Operations:** 8 tools + - Glob, Grep, LS, and 5 more +- **Git/GitHub:** 80 tools + - github::add_comment_to_pending_review, github::add_issue_comment, github::add_sub_issue, and 77 more +- **MCP:** 2 tools + - ListMcpResourcesTool, ReadMcpResourceTool +- **Other:** 3 tools + - WebFetch, TodoWrite, WebSearch + +**Slash Commands:** 34 available +- add-dir, agents, clear, compact, config, and 29 more + + ## šŸ¤– Commands and Tools * āœ… `echo $GITHUB_STEP_SUMMARY` From 198e46064560e9ceb9e450434bf8b8eb236b9a97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 20:25:38 +0000 Subject: [PATCH 03/10] Fix TypeScript type errors in Claude log parser Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/ci-doctor.lock.yml | 1 + pkg/workflow/js/parse_claude_log.cjs | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index 7c2652a74a3..37ae6590cdc 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -1520,6 +1520,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], diff --git a/pkg/workflow/js/parse_claude_log.cjs b/pkg/workflow/js/parse_claude_log.cjs index 77c3215f954..2115b8bc69a 100644 --- a/pkg/workflow/js/parse_claude_log.cjs +++ b/pkg/workflow/js/parse_claude_log.cjs @@ -249,6 +249,7 @@ function formatInitializationSummary(initEntry) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], From 04f298c7c19d69c596c6f9fc88644e9061da18ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 20:37:35 +0000 Subject: [PATCH 04/10] Add MCP server failure detection to Claude log parser Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/js/parse_claude_log.cjs | 40 +++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/pkg/workflow/js/parse_claude_log.cjs b/pkg/workflow/js/parse_claude_log.cjs index 2115b8bc69a..cadbae70fb2 100644 --- a/pkg/workflow/js/parse_claude_log.cjs +++ b/pkg/workflow/js/parse_claude_log.cjs @@ -15,10 +15,16 @@ function main() { } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -28,16 +34,21 @@ function main() { /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( @@ -46,7 +57,9 @@ function parseClaudeLog(logContent) { if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } @@ -196,20 +209,24 @@ function parseClaudeLog(logContent) { } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { @@ -240,6 +257,11 @@ function formatInitializationSummary(initEntry) { ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -320,7 +342,7 @@ function formatInitializationSummary(initEntry) { markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** From e6a0a2711d51350c20bfd91220407c779198d5ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 20:46:26 +0000 Subject: [PATCH 05/10] Add MCP server failure detection and reporting to logs command Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/ci-doctor.lock.yml | 38 +++- pkg/cli/logs.go | 275 +++++++++++++++++++++++++++ 2 files changed, 304 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index 37ae6590cdc..d0af43b9898 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -1314,9 +1314,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -1325,22 +1330,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -1474,19 +1486,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -1513,6 +1529,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -1587,7 +1607,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/logs.go b/pkg/cli/logs.go index 0c826840b4a..882c99d92a4 100644 --- a/pkg/cli/logs.go +++ b/pkg/cli/logs.go @@ -51,6 +51,7 @@ type ProcessedRun struct { Run WorkflowRun AccessAnalysis *DomainAnalysis MissingTools []MissingToolReport + MCPFailures []MCPFailureReport } // MissingToolReport represents a missing tool reported by an agentic workflow @@ -63,6 +64,15 @@ type MissingToolReport struct { RunID int64 `json:"run_id,omitempty"` // Added for tracking which run reported this } +// MCPFailureReport represents an MCP server failure detected in a workflow run +type MCPFailureReport struct { + ServerName string `json:"server_name"` + Status string `json:"status"` + Timestamp string `json:"timestamp,omitempty"` + WorkflowName string `json:"workflow_name,omitempty"` + RunID int64 `json:"run_id,omitempty"` +} + // MissingToolSummary aggregates missing tool reports across runs type MissingToolSummary struct { Tool string @@ -81,6 +91,7 @@ type DownloadResult struct { Metrics LogMetrics AccessAnalysis *DomainAnalysis MissingTools []MissingToolReport + MCPFailures []MCPFailureReport Error error Skipped bool LogsPath string @@ -398,6 +409,7 @@ func DownloadWorkflowLogs(workflowName string, count int, startDate, endDate, ou Run: run, AccessAnalysis: result.AccessAnalysis, MissingTools: result.MissingTools, + MCPFailures: result.MCPFailures, } processedRuns = append(processedRuns, processedRun) batchProcessed++ @@ -448,6 +460,9 @@ func DownloadWorkflowLogs(workflowName string, count int, startDate, endDate, ou // Display missing tools analysis displayMissingToolsAnalysis(processedRuns, verbose) + // Display MCP failures analysis + displayMCPFailuresAnalysis(processedRuns, verbose) + // Generate tool sequence graph if requested if toolGraph { generateToolGraph(processedRuns, verbose) @@ -532,6 +547,15 @@ func downloadRunArtifactsConcurrent(runs []WorkflowRun, outputDir string, verbos } } result.MissingTools = missingTools + + // Extract MCP failures if available + mcpFailures, mcpErr := extractMCPFailuresFromRun(runOutputDir, run, verbose) + if mcpErr != nil { + if verbose { + fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Failed to extract MCP failures for run %d: %v", run.DatabaseID, mcpErr))) + } + } + result.MCPFailures = mcpFailures } return result @@ -1547,3 +1571,254 @@ func displayDetailedMissingToolsBreakdown(processedRuns []ProcessedRun) { } } } + +// extractMCPFailuresFromRun extracts MCP server failure reports from a workflow run's logs +func extractMCPFailuresFromRun(runDir string, run WorkflowRun, verbose bool) ([]MCPFailureReport, error) { + var mcpFailures []MCPFailureReport + + // Look for agent output logs that contain the system init entry with MCP server status + // This information is available in the raw log files, typically with names containing "log" + err := filepath.Walk(runDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip directories + if info.IsDir() { + return nil + } + + // Process log files - exclude output artifacts + fileName := strings.ToLower(info.Name()) + if (strings.HasSuffix(fileName, ".log") || + (strings.HasSuffix(fileName, ".txt") && strings.Contains(fileName, "log"))) && + !strings.Contains(fileName, "aw_output") && + !strings.Contains(fileName, "agent_output") && + !strings.Contains(fileName, "access") { + + // Parse this log file for MCP server failures + failures, parseErr := extractMCPFailuresFromLogFile(path, run, verbose) + if parseErr != nil { + if verbose { + fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Failed to parse MCP failures from %s: %v", filepath.Base(path), parseErr))) + } + return nil // Continue processing other files + } + mcpFailures = append(mcpFailures, failures...) + } + + return nil + }) + + if err != nil { + return mcpFailures, fmt.Errorf("error walking run directory: %w", err) + } + + if verbose && len(mcpFailures) > 0 { + fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Found %d MCP server failures for run %d", len(mcpFailures), run.DatabaseID))) + } + + return mcpFailures, nil +} + +// extractMCPFailuresFromLogFile parses a single log file for MCP server failures +func extractMCPFailuresFromLogFile(logPath string, run WorkflowRun, verbose bool) ([]MCPFailureReport, error) { + var mcpFailures []MCPFailureReport + + content, err := os.ReadFile(logPath) + if err != nil { + return mcpFailures, fmt.Errorf("error reading log file: %w", err) + } + + logContent := string(content) + + // Try to parse as JSON lines (Claude logs are typically NDJSON format) + lines := strings.Split(logContent, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || !strings.HasPrefix(line, "{") { + continue + } + + // Try to parse each line as JSON + var entry map[string]interface{} + if err := json.Unmarshal([]byte(line), &entry); err != nil { + continue // Skip non-JSON lines + } + + // Look for system init entries that contain MCP server information + if entryType, ok := entry["type"].(string); ok && entryType == "system" { + if subtype, ok := entry["subtype"].(string); ok && subtype == "init" { + // Extract MCP server failures from this init entry + if mcpServers, ok := entry["mcp_servers"].([]interface{}); ok { + for _, serverInterface := range mcpServers { + if server, ok := serverInterface.(map[string]interface{}); ok { + serverName, hasName := server["name"].(string) + status, hasStatus := server["status"].(string) + + if hasName && hasStatus && status == "failed" { + failure := MCPFailureReport{ + ServerName: serverName, + Status: status, + WorkflowName: run.WorkflowName, + RunID: run.DatabaseID, + } + + // Try to extract timestamp if available + if timestamp, hasTimestamp := entry["timestamp"].(string); hasTimestamp { + failure.Timestamp = timestamp + } + + mcpFailures = append(mcpFailures, failure) + + if verbose { + fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Found MCP server failure: %s (status: %s)", serverName, status))) + } + } + } + } + } + } + } + } + + return mcpFailures, nil +} + +// MCPFailureSummary aggregates MCP server failures across runs +type MCPFailureSummary struct { + ServerName string + Count int + Workflows []string // List of workflow names that had this server fail + RunIDs []int64 // List of run IDs where this server failed +} + +// displayMCPFailuresAnalysis displays a summary of MCP server failures across all runs +func displayMCPFailuresAnalysis(processedRuns []ProcessedRun, verbose bool) { + // Aggregate MCP failures across all runs + failureSummary := make(map[string]*MCPFailureSummary) + var totalFailures int + + for _, pr := range processedRuns { + for _, failure := range pr.MCPFailures { + totalFailures++ + if summary, exists := failureSummary[failure.ServerName]; exists { + summary.Count++ + // Add workflow if not already in the list + found := false + for _, wf := range summary.Workflows { + if wf == failure.WorkflowName { + found = true + break + } + } + if !found { + summary.Workflows = append(summary.Workflows, failure.WorkflowName) + } + summary.RunIDs = append(summary.RunIDs, failure.RunID) + } else { + failureSummary[failure.ServerName] = &MCPFailureSummary{ + ServerName: failure.ServerName, + Count: 1, + Workflows: []string{failure.WorkflowName}, + RunIDs: []int64{failure.RunID}, + } + } + } + } + + if totalFailures == 0 { + return // No MCP failures to display + } + + // Display summary header + fmt.Printf("\n%s\n", console.FormatListHeader("šŸ”Œ MCP Server Failures")) + fmt.Printf("%s\n\n", console.FormatListHeader("=====================")) + + // Convert map to slice for sorting + var summaries []*MCPFailureSummary + for _, summary := range failureSummary { + summaries = append(summaries, summary) + } + + // Sort by count (descending) + sort.Slice(summaries, func(i, j int) bool { + return summaries[i].Count > summaries[j].Count + }) + + // Display summary table + headers := []string{"MCP Server", "Failures", "Workflows", "Run IDs"} + var rows [][]string + + for _, summary := range summaries { + workflowList := strings.Join(summary.Workflows, ", ") + if len(workflowList) > 30 { + workflowList = workflowList[:27] + "..." + } + + runIDStrs := make([]string, len(summary.RunIDs)) + for i, runID := range summary.RunIDs { + runIDStrs[i] = fmt.Sprintf("%d", runID) + } + runIDList := strings.Join(runIDStrs, ", ") + if len(runIDList) > 40 { + runIDList = runIDList[:37] + "..." + } + + rows = append(rows, []string{ + summary.ServerName, + fmt.Sprintf("%d", summary.Count), + workflowList, + runIDList, + }) + } + + tableConfig := console.TableConfig{ + Headers: headers, + Rows: rows, + } + + fmt.Print(console.RenderTable(tableConfig)) + + // Display total summary + uniqueServers := len(failureSummary) + fmt.Printf("\nšŸ“Š %s: %d unique MCP servers failed %d times across workflows\n", + console.FormatCountMessage("Total"), + uniqueServers, + totalFailures) + + // Verbose mode: Show detailed breakdown by workflow + if verbose && totalFailures > 0 { + displayDetailedMCPFailuresBreakdown(processedRuns) + } +} + +// displayDetailedMCPFailuresBreakdown shows MCP failures organized by workflow (verbose mode) +func displayDetailedMCPFailuresBreakdown(processedRuns []ProcessedRun) { + fmt.Printf("\n%s\n", console.FormatListHeader("šŸ” Detailed MCP Failures Breakdown")) + fmt.Printf("%s\n", console.FormatListHeader("===================================")) + + for _, pr := range processedRuns { + if len(pr.MCPFailures) == 0 { + continue + } + + fmt.Printf("\n%s (Run %d) - %d failed MCP servers:\n", + console.FormatInfoMessage(pr.Run.WorkflowName), + pr.Run.DatabaseID, + len(pr.MCPFailures)) + + for i, failure := range pr.MCPFailures { + fmt.Printf(" %d. %s %s\n", + i+1, + console.FormatListItem(failure.ServerName), + console.FormatErrorMessage(fmt.Sprintf("- Status: %s", failure.Status))) + + if failure.Timestamp != "" { + fmt.Printf(" %s %s\n", + console.FormatVerboseMessage("Failed at:"), + failure.Timestamp) + } + } + } +} From 97c872d9e6a70e7e6f58b49ea9af6fa0d1a99bb5 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Sat, 13 Sep 2025 22:06:57 +0000 Subject: [PATCH 06/10] Enhance MCP server failure handling in Claude log parsing workflows - Updated log parsing functions to return both formatted markdown and a list of MCP server failures. - Modified workflows to check for MCP server failures after parsing logs and fail the job if any are detected. - Ensured consistent handling of log content across multiple workflow files, improving error reporting and user feedback. --- Makefile | 5 +-- .../test-ai-inference-github-models.lock.yml | 39 ++++++++++++++----- .../test-claude-add-issue-comment.lock.yml | 39 ++++++++++++++----- .../test-claude-add-issue-labels.lock.yml | 39 ++++++++++++++----- .../workflows/test-claude-command.lock.yml | 39 ++++++++++++++----- .../test-claude-create-issue.lock.yml | 39 ++++++++++++++----- ...reate-pull-request-review-comment.lock.yml | 39 ++++++++++++++----- .../test-claude-create-pull-request.lock.yml | 39 ++++++++++++++----- ...eate-repository-security-advisory.lock.yml | 39 ++++++++++++++----- pkg/cli/workflows/test-claude-mcp.lock.yml | 39 ++++++++++++++----- .../test-claude-push-to-pr-branch.lock.yml | 39 ++++++++++++++----- .../test-claude-update-issue.lock.yml | 39 ++++++++++++++----- ...playwright-accessibility-contrast.lock.yml | 39 ++++++++++++++----- 13 files changed, 361 insertions(+), 112 deletions(-) diff --git a/Makefile b/Makefile index 707b40d26fd..cb1b307b4f4 100644 --- a/Makefile +++ b/Makefile @@ -152,10 +152,7 @@ install: build .PHONY: recompile recompile: build ./$(BINARY_NAME) compile --validate --instructions - @if [ -d "pkg/cli/workflows" ]; then \ - echo "Compiling test workflows from pkg/cli/workflows..."; \ - ./$(BINARY_NAME) compile --workflow-dir pkg/cli/workflows --validate; \ - fi + ./$(BINARY_NAME) compile --workflow-dir pkg/cli/workflows --validate; # Run development server .PHONY: dev diff --git a/pkg/cli/workflows/test-ai-inference-github-models.lock.yml b/pkg/cli/workflows/test-ai-inference-github-models.lock.yml index c58edfaf7af..f47ded1eec7 100644 --- a/pkg/cli/workflows/test-ai-inference-github-models.lock.yml +++ b/pkg/cli/workflows/test-ai-inference-github-models.lock.yml @@ -324,9 +324,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -335,22 +340,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -484,19 +496,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -523,6 +539,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -530,6 +550,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -596,7 +617,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/workflows/test-claude-add-issue-comment.lock.yml b/pkg/cli/workflows/test-claude-add-issue-comment.lock.yml index d907b84e6dc..b32440ffef8 100644 --- a/pkg/cli/workflows/test-claude-add-issue-comment.lock.yml +++ b/pkg/cli/workflows/test-claude-add-issue-comment.lock.yml @@ -321,9 +321,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -332,22 +337,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -481,19 +493,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -520,6 +536,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -527,6 +547,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -593,7 +614,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/workflows/test-claude-add-issue-labels.lock.yml b/pkg/cli/workflows/test-claude-add-issue-labels.lock.yml index 2020a8d7047..a1291cad4ef 100644 --- a/pkg/cli/workflows/test-claude-add-issue-labels.lock.yml +++ b/pkg/cli/workflows/test-claude-add-issue-labels.lock.yml @@ -321,9 +321,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -332,22 +337,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -481,19 +493,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -520,6 +536,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -527,6 +547,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -593,7 +614,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/workflows/test-claude-command.lock.yml b/pkg/cli/workflows/test-claude-command.lock.yml index 26cf9a8830a..7ff15e48a67 100644 --- a/pkg/cli/workflows/test-claude-command.lock.yml +++ b/pkg/cli/workflows/test-claude-command.lock.yml @@ -321,9 +321,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -332,22 +337,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -481,19 +493,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -520,6 +536,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -527,6 +547,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -593,7 +614,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/workflows/test-claude-create-issue.lock.yml b/pkg/cli/workflows/test-claude-create-issue.lock.yml index f39eda5c83c..3b6fe8af562 100644 --- a/pkg/cli/workflows/test-claude-create-issue.lock.yml +++ b/pkg/cli/workflows/test-claude-create-issue.lock.yml @@ -321,9 +321,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -332,22 +337,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -481,19 +493,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -520,6 +536,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -527,6 +547,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -593,7 +614,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/workflows/test-claude-create-pull-request-review-comment.lock.yml b/pkg/cli/workflows/test-claude-create-pull-request-review-comment.lock.yml index 95295d8c480..659e407da6a 100644 --- a/pkg/cli/workflows/test-claude-create-pull-request-review-comment.lock.yml +++ b/pkg/cli/workflows/test-claude-create-pull-request-review-comment.lock.yml @@ -321,9 +321,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -332,22 +337,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -481,19 +493,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -520,6 +536,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -527,6 +547,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -593,7 +614,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/workflows/test-claude-create-pull-request.lock.yml b/pkg/cli/workflows/test-claude-create-pull-request.lock.yml index 72086319423..95fad0c316f 100644 --- a/pkg/cli/workflows/test-claude-create-pull-request.lock.yml +++ b/pkg/cli/workflows/test-claude-create-pull-request.lock.yml @@ -326,9 +326,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -337,22 +342,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -486,19 +498,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -525,6 +541,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -532,6 +552,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -598,7 +619,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/workflows/test-claude-create-repository-security-advisory.lock.yml b/pkg/cli/workflows/test-claude-create-repository-security-advisory.lock.yml index 8e98fb6b4b6..aa6a714a97f 100644 --- a/pkg/cli/workflows/test-claude-create-repository-security-advisory.lock.yml +++ b/pkg/cli/workflows/test-claude-create-repository-security-advisory.lock.yml @@ -324,9 +324,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -335,22 +340,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -484,19 +496,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -523,6 +539,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -530,6 +550,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -596,7 +617,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/workflows/test-claude-mcp.lock.yml b/pkg/cli/workflows/test-claude-mcp.lock.yml index 0d2a588b93a..f6cb2bbd5cf 100644 --- a/pkg/cli/workflows/test-claude-mcp.lock.yml +++ b/pkg/cli/workflows/test-claude-mcp.lock.yml @@ -324,9 +324,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -335,22 +340,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -484,19 +496,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -523,6 +539,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -530,6 +550,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -596,7 +617,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/workflows/test-claude-push-to-pr-branch.lock.yml b/pkg/cli/workflows/test-claude-push-to-pr-branch.lock.yml index 1277f817148..1edd439bb10 100644 --- a/pkg/cli/workflows/test-claude-push-to-pr-branch.lock.yml +++ b/pkg/cli/workflows/test-claude-push-to-pr-branch.lock.yml @@ -326,9 +326,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -337,22 +342,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -486,19 +498,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -525,6 +541,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -532,6 +552,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -598,7 +619,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/workflows/test-claude-update-issue.lock.yml b/pkg/cli/workflows/test-claude-update-issue.lock.yml index 097ef99a99e..677f52aaec3 100644 --- a/pkg/cli/workflows/test-claude-update-issue.lock.yml +++ b/pkg/cli/workflows/test-claude-update-issue.lock.yml @@ -324,9 +324,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -335,22 +340,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -484,19 +496,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -523,6 +539,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -530,6 +550,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -596,7 +617,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown diff --git a/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml b/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml index 043f0fd4f75..6b925e2f6c5 100644 --- a/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml +++ b/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml @@ -1239,9 +1239,14 @@ jobs: return; } const logContent = fs.readFileSync(logFile, "utf8"); - const markdown = parseClaudeLog(logContent); + const result = parseClaudeLog(logContent); // Append to GitHub step summary - core.summary.addRaw(markdown).write(); + core.summary.addRaw(result.markdown).write(); + // Check for MCP server failures and fail the job if any occurred + if (result.mcpFailures && result.mcpFailures.length > 0) { + const failedServers = result.mcpFailures.join(", "); + core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); + } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.setFailed(errorMessage); @@ -1250,22 +1255,29 @@ jobs: /** * Parses Claude log content and converts it to markdown format * @param {string} logContent - The raw log content as a string - * @returns {string} Formatted markdown content + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown content and MCP failure list */ function parseClaudeLog(logContent) { try { const logEntries = JSON.parse(logContent); if (!Array.isArray(logEntries)) { - return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + return { + markdown: + "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n", + mcpFailures: [], + }; } let markdown = ""; + const mcpFailures = []; // Check for initialization data first const initEntry = logEntries.find( entry => entry.type === "system" && entry.subtype === "init" ); if (initEntry) { markdown += "## šŸš€ Initialization\n\n"; - markdown += formatInitializationSummary(initEntry); + const initResult = formatInitializationSummary(initEntry); + markdown += initResult.markdown; + mcpFailures.push(...initResult.mcpFailures); markdown += "\n"; } markdown += "## šŸ¤– Commands and Tools\n\n"; @@ -1399,19 +1411,23 @@ jobs: } } } - return markdown; + return { markdown, mcpFailures }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - return `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`; + return { + markdown: `## Agent Log Summary\n\nError parsing Claude log: ${errorMessage}\n`, + mcpFailures: [], + }; } } /** * Formats initialization information from system init entry * @param {any} initEntry - The system init entry containing tools, mcp_servers, etc. - * @returns {string} Formatted markdown string with initialization summary + * @returns {{markdown: string, mcpFailures: string[]}} Result with formatted markdown string and MCP failure list */ function formatInitializationSummary(initEntry) { let markdown = ""; + const mcpFailures = []; // Display model and session info if (initEntry.model) { markdown += `**Model:** ${initEntry.model}\n\n`; @@ -1438,6 +1454,10 @@ jobs: ? "āŒ" : "ā“"; markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; + // Track failed MCP servers + if (server.status === "failed") { + mcpFailures.push(server.name); + } } markdown += "\n"; } @@ -1445,6 +1465,7 @@ jobs: if (initEntry.tools && Array.isArray(initEntry.tools)) { markdown += "**Available Tools:**\n"; // Categorize tools + /** @type {{ [key: string]: string[] }} */ const categories = { Core: [], "File Operations": [], @@ -1511,7 +1532,7 @@ jobs: } markdown += "\n"; } - return markdown; + return { markdown, mcpFailures }; } /** * Formats a tool use entry with its result into markdown From 09235b8d8aae2215c76df54516f915360755378d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 22:30:44 +0000 Subject: [PATCH 07/10] Add comprehensive test coverage for MCP server failure detection in logs command - Add TestExtractMCPFailuresFromSafeOutputsServer to test the exact failure scenario from GitHub Actions job 50307041425 - Add TestExtractMCPFailuresFromLogFileDirectly to test direct log file parsing for MCP failures - Both tests verify that the Go parser correctly detects when the safe_outputs MCP server fails to launch - Tests confirm the logs command can properly identify MCP server failures with status "failed" - Validates the complete MCP failure reporting workflow including server name, status, workflow name, and run ID extraction Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/logs_mcp_failure_test.go | 103 +++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 pkg/cli/logs_mcp_failure_test.go diff --git a/pkg/cli/logs_mcp_failure_test.go b/pkg/cli/logs_mcp_failure_test.go new file mode 100644 index 00000000000..b8ab960dec6 --- /dev/null +++ b/pkg/cli/logs_mcp_failure_test.go @@ -0,0 +1,103 @@ +package cli + +import ( + "os" + "path/filepath" + "testing" +) + +func TestExtractMCPFailuresFromSafeOutputsServer(t *testing.T) { + // Test specific scenario from https://github.com/githubnext/gh-aw/actions/runs/17701181429/job/50307041425 + // where safe_outputs MCP server failed to launch + logContent := `{"type":"system","subtype":"init","cwd":"/home/runner/work/gh-aw/gh-aw","session_id":"d5d6d3c4-b04d-4b41-ba8e-6de4e93648bb","tools":["Task","Bash","Glob","Grep","LS","ExitPlanMode","Read","Edit","MultiEdit","Write","NotebookEdit","WebFetch","TodoWrite","WebSearch","BashOutput","KillBash","mcp__github__add_comment_to_pending_review","mcp__github__add_issue_comment","mcp__github__add_sub_issue","mcp__github__assign_copilot_to_issue","mcp__github__cancel_workflow_run","mcp__github__create_and_submit_pull_request_review","mcp__github__create_branch","mcp__github__create_gist","mcp__github__create_issue","mcp__github__create_or_update_file","mcp__github__create_pending_pull_request_review","mcp__github__create_pull_request","mcp__github__create_repository","mcp__github__delete_file","mcp__github__delete_pending_pull_request_review","mcp__github__delete_workflow_run_logs","mcp__github__dismiss_notification","mcp__github__download_workflow_run_artifact","mcp__github__fork_repository","mcp__github__get_code_scanning_alert","mcp__github__get_commit","mcp__github__get_dependabot_alert","mcp__github__get_discussion","mcp__github__get_discussion_comments","mcp__github__get_file_contents","mcp__github__get_global_security_advisory","mcp__github__get_issue","mcp__github__get_issue_comments","mcp__github__get_job_logs","mcp__github__get_latest_release","mcp__github__get_me","mcp__github__get_notification_details","mcp__github__get_pull_request","mcp__github__get_pull_request_comments","mcp__github__get_pull_request_diff","mcp__github__get_pull_request_files","mcp__github__get_pull_request_reviews","mcp__github__get_pull_request_status","mcp__github__get_release_by_tag","mcp__github__get_secret_scanning_alert","mcp__github__get_tag","mcp__github__get_team_members","mcp__github__get_teams","mcp__github__get_workflow_run","mcp__github__get_workflow_run_logs","mcp__github__get_workflow_run_usage","mcp__github__list_branches","mcp__github__list_code_scanning_alerts","mcp__github__list_commits","mcp__github__list_dependabot_alerts","mcp__github__list_discussion_categories","mcp__github__list_discussions","mcp__github__list_gists","mcp__github__list_global_security_advisories","mcp__github__list_issue_types","mcp__github__list_issues","mcp__github__list_notifications","mcp__github__list_org_repository_security_advisories","mcp__github__list_pull_requests","mcp__github__list_releases","mcp__github__list_repository_security_advisories","mcp__github__list_secret_scanning_alerts","mcp__github__list_sub_issues","mcp__github__list_tags","mcp__github__list_workflow_jobs","mcp__github__list_workflow_run_artifacts","mcp__github__list_workflow_runs","mcp__github__list_workflows","mcp__github__manage_notification_subscription","mcp__github__manage_repository_notification_subscription","mcp__github__mark_all_notifications_read","mcp__github__merge_pull_request","mcp__github__push_files","mcp__github__remove_sub_issue","mcp__github__reprioritize_sub_issue","mcp__github__request_copilot_review","mcp__github__rerun_failed_jobs","mcp__github__rerun_workflow_run","mcp__github__run_workflow","mcp__github__search_code","mcp__github__search_issues","mcp__github__search_orgs","mcp__github__search_pull_requests","mcp__github__search_repositories","mcp__github__search_users","mcp__github__submit_pending_pull_request_review","mcp__github__update_gist","mcp__github__update_issue","mcp__github__update_pull_request","mcp__github__update_pull_request_branch","ListMcpResourcesTool","ReadMcpResourceTool"],"mcp_servers":[{"name":"github","status":"connected"},{"name":"safe_outputs","status":"failed"}],"model":"claude-sonnet-4-20250514","permissionMode":"default","slash_commands":["add-dir","agents","clear","compact","config","cost","doctor","exit","help","ide","init","install-github-app","mcp","memory","migrate-installer","model","pr-comments","release-notes","resume","status","statusline","bug","review","security-review","upgrade","vim","permissions","hooks","export","logout","login","bashes","mcp__github__AssignCodingAgent","mcp__github__IssueToFixWorkflow"],"apiKeySource":"ANTHROPIC_API_KEY"} +{"type":"assistant","message":{"id":"msg_01McAF6aPVtSGqp4RFYp2ugV","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"I cannot call a ` + "`draw pelican`" + ` tool or any other missing tool, as I don't have access to tools that don't exist in my available toolset. Additionally, I don't have a ` + "`missing-tool`" + ` tool available to report this missing functionality.\\n\\nThe tools I have access to are the standard file system operations (Read, Write, Edit, etc.), GitHub API tools (mcp__github__*), search tools (Grep, Glob), and other development utilities listed in my function definitions.\\n\\nIf you need to test the missing-tool safe output functionality as described in the workflow, you would need to run the actual GitHub Actions workflow that contains the custom engine implementation shown in the markdown file."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":39481,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":39481,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard"}},"parent_tool_use_id":null,"session_id":"d5d6d3c4-b04d-4b41-ba8e-6de4e93648bb"} +{"type":"result","subtype":"success","is_error":false,"duration_ms":7173,"duration_api_ms":8795,"num_turns":1,"result":"I cannot call a ` + "`draw pelican`" + ` tool or any other missing tool, as I don't have access to tools that don't exist in my available toolset. Additionally, I don't have a ` + "`missing-tool`" + ` tool available to report this missing functionality.\\n\\nThe tools I have access to are the standard file system operations (Read, Write, Edit, etc.), GitHub API tools (mcp__github__*), search tools (Grep, Glob), and other development utilities listed in my function definitions.\\n\\nIf you need to test the missing-tool safe output functionality as described in the workflow, you would need to run the actual GitHub Actions workflow that contains the custom engine implementation shown in the markdown file.","session_id":"d5d6d3c4-b04d-4b41-ba8e-6de4e93648bb","total_cost_usd":0.2987599,"usage":{"input_tokens":6,"cache_creation_input_tokens":78962,"cache_read_input_tokens":0,"output_tokens":152,"server_tool_use":{"web_search_requests":0},"service_tier":"standard"},"permission_denials":[]}` + + // Create a temporary directory structure + tmpDir := t.TempDir() + runDir := filepath.Join(tmpDir, "run-17701181429") + err := os.MkdirAll(runDir, 0755) + if err != nil { + t.Fatalf("Failed to create run directory: %v", err) + } + + // Create the log file + logFile := filepath.Join(runDir, "test-safe-output-missing-tool.log") + err = os.WriteFile(logFile, []byte(logContent), 0644) + if err != nil { + t.Fatalf("Failed to write log file: %v", err) + } + + // Test run information + run := WorkflowRun{ + DatabaseID: 17701181429, + WorkflowName: "Test Safe Output - Missing Tool", + } + + // Test the extraction function + failures, err := extractMCPFailuresFromRun(runDir, run, true) + if err != nil { + t.Fatalf("Failed to extract MCP failures: %v", err) + } + + // Verify that we detected the safe_outputs server failure + if len(failures) == 0 { + t.Fatal("Expected to find MCP failures, but found none") + } + + if len(failures) != 1 { + t.Fatalf("Expected to find 1 MCP failure, but found %d", len(failures)) + } + + failure := failures[0] + if failure.ServerName != "safe_outputs" { + t.Errorf("Expected server name 'safe_outputs', got '%s'", failure.ServerName) + } + + if failure.Status != "failed" { + t.Errorf("Expected status 'failed', got '%s'", failure.Status) + } + + if failure.WorkflowName != "Test Safe Output - Missing Tool" { + t.Errorf("Expected workflow name 'Test Safe Output - Missing Tool', got '%s'", failure.WorkflowName) + } + + if failure.RunID != 17701181429 { + t.Errorf("Expected run ID 17701181429, got %d", failure.RunID) + } +} + +func TestExtractMCPFailuresFromLogFileDirectly(t *testing.T) { + // Test the direct log file parsing with the exact content + logContent := `{"type":"system","subtype":"init","cwd":"/home/runner/work/gh-aw/gh-aw","session_id":"d5d6d3c4-b04d-4b41-ba8e-6de4e93648bb","mcp_servers":[{"name":"github","status":"connected"},{"name":"safe_outputs","status":"failed"}],"model":"claude-sonnet-4-20250514"}` + + run := WorkflowRun{ + DatabaseID: 17701181429, + WorkflowName: "Test Safe Output - Missing Tool", + } + + // Create a temporary file for this test + tmpFile := filepath.Join(t.TempDir(), "test.log") + err := os.WriteFile(tmpFile, []byte(logContent), 0644) + if err != nil { + t.Fatalf("Failed to write temporary log file: %v", err) + } + + failures, err := extractMCPFailuresFromLogFile(tmpFile, run, true) + if err != nil { + t.Fatalf("Failed to extract MCP failures from log file: %v", err) + } + + if len(failures) != 1 { + t.Fatalf("Expected to find 1 MCP failure, but found %d", len(failures)) + } + + failure := failures[0] + if failure.ServerName != "safe_outputs" { + t.Errorf("Expected server name 'safe_outputs', got '%s'", failure.ServerName) + } + + if failure.Status != "failed" { + t.Errorf("Expected status 'failed', got '%s'", failure.Status) + } +} From 9242bd2af3604309dbc3b7c08bc165141df7c30f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 22:57:23 +0000 Subject: [PATCH 08/10] Add prominent MCP failure alert to logs command output Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/logs.go | 10 ++++++++++ pkg/workflow/log_parser_snapshot_test.go | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pkg/cli/logs.go b/pkg/cli/logs.go index 882c99d92a4..6a5d01445cd 100644 --- a/pkg/cli/logs.go +++ b/pkg/cli/logs.go @@ -451,6 +451,16 @@ func DownloadWorkflowLogs(workflowName string, count int, startDate, endDate, ou } displayLogsOverview(workflowRuns) + // Check for and prominently display MCP failures if any are detected + totalMCPFailures := 0 + for _, pr := range processedRuns { + totalMCPFailures += len(pr.MCPFailures) + } + if totalMCPFailures > 0 { + fmt.Printf("\n%s\n", console.FormatErrorMessage(fmt.Sprintf("āš ļø ALERT: %d MCP server failure(s) detected across analyzed runs", totalMCPFailures))) + fmt.Printf("%s\n", console.FormatWarningMessage("MCP server failures can cause workflow execution issues. See detailed report below.")) + } + // Display tool call report displayToolCallReport(processedRuns, verbose) diff --git a/pkg/workflow/log_parser_snapshot_test.go b/pkg/workflow/log_parser_snapshot_test.go index aed26a18810..fb75abb1dc5 100644 --- a/pkg/workflow/log_parser_snapshot_test.go +++ b/pkg/workflow/log_parser_snapshot_test.go @@ -110,7 +110,12 @@ const core = { }, setFailed: function(message) { console.error('FAILED:', message); - process.exit(1); + // Don't exit in test - just mark the failure + this._failed = true; + this._failureMessage = message; + }, + info: function(message) { + // Mock info function for testing } }; From 42ce82724a53c9280b425c6b9ecf0aca243e459f Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Sat, 13 Sep 2025 23:41:51 +0000 Subject: [PATCH 09/10] fix log parsing logic --- pkg/cli/logs.go | 117 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 79 insertions(+), 38 deletions(-) diff --git a/pkg/cli/logs.go b/pkg/cli/logs.go index 6a5d01445cd..0b4c3548409 100644 --- a/pkg/cli/logs.go +++ b/pkg/cli/logs.go @@ -1642,47 +1642,88 @@ func extractMCPFailuresFromLogFile(logPath string, run WorkflowRun, verbose bool logContent := string(content) - // Try to parse as JSON lines (Claude logs are typically NDJSON format) - lines := strings.Split(logContent, "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" || !strings.HasPrefix(line, "{") { - continue - } - - // Try to parse each line as JSON - var entry map[string]interface{} - if err := json.Unmarshal([]byte(line), &entry); err != nil { - continue // Skip non-JSON lines - } - - // Look for system init entries that contain MCP server information - if entryType, ok := entry["type"].(string); ok && entryType == "system" { - if subtype, ok := entry["subtype"].(string); ok && subtype == "init" { - // Extract MCP server failures from this init entry - if mcpServers, ok := entry["mcp_servers"].([]interface{}); ok { - for _, serverInterface := range mcpServers { - if server, ok := serverInterface.(map[string]interface{}); ok { - serverName, hasName := server["name"].(string) - status, hasStatus := server["status"].(string) - - if hasName && hasStatus && status == "failed" { - failure := MCPFailureReport{ - ServerName: serverName, - Status: status, - WorkflowName: run.WorkflowName, - RunID: run.DatabaseID, - } - - // Try to extract timestamp if available - if timestamp, hasTimestamp := entry["timestamp"].(string); hasTimestamp { - failure.Timestamp = timestamp + // First try to parse as JSON array + var logEntries []map[string]interface{} + if err := json.Unmarshal(content, &logEntries); err == nil { + // Successfully parsed as JSON array, process entries + for _, entry := range logEntries { + if entryType, ok := entry["type"].(string); ok && entryType == "system" { + if subtype, ok := entry["subtype"].(string); ok && subtype == "init" { + // Extract MCP server failures from this init entry + if mcpServers, ok := entry["mcp_servers"].([]interface{}); ok { + for _, serverInterface := range mcpServers { + if server, ok := serverInterface.(map[string]interface{}); ok { + serverName, hasName := server["name"].(string) + status, hasStatus := server["status"].(string) + + if hasName && hasStatus && status == "failed" { + failure := MCPFailureReport{ + ServerName: serverName, + Status: status, + WorkflowName: run.WorkflowName, + RunID: run.DatabaseID, + } + + // Try to extract timestamp if available + if timestamp, hasTimestamp := entry["timestamp"].(string); hasTimestamp { + failure.Timestamp = timestamp + } + + mcpFailures = append(mcpFailures, failure) + + if verbose { + fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Found MCP server failure: %s (status: %s)", serverName, status))) + } } + } + } + } + } + } + } + } else { + // Fallback: Try to parse as JSON lines (Claude logs are typically NDJSON format) + lines := strings.Split(logContent, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || !strings.HasPrefix(line, "{") { + continue + } - mcpFailures = append(mcpFailures, failure) + // Try to parse each line as JSON + var entry map[string]interface{} + if err := json.Unmarshal([]byte(line), &entry); err != nil { + continue // Skip non-JSON lines + } - if verbose { - fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Found MCP server failure: %s (status: %s)", serverName, status))) + // Look for system init entries that contain MCP server information + if entryType, ok := entry["type"].(string); ok && entryType == "system" { + if subtype, ok := entry["subtype"].(string); ok && subtype == "init" { + // Extract MCP server failures from this init entry + if mcpServers, ok := entry["mcp_servers"].([]interface{}); ok { + for _, serverInterface := range mcpServers { + if server, ok := serverInterface.(map[string]interface{}); ok { + serverName, hasName := server["name"].(string) + status, hasStatus := server["status"].(string) + + if hasName && hasStatus && status == "failed" { + failure := MCPFailureReport{ + ServerName: serverName, + Status: status, + WorkflowName: run.WorkflowName, + RunID: run.DatabaseID, + } + + // Try to extract timestamp if available + if timestamp, hasTimestamp := entry["timestamp"].(string); hasTimestamp { + failure.Timestamp = timestamp + } + + mcpFailures = append(mcpFailures, failure) + + if verbose { + fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Found MCP server failure: %s (status: %s)", serverName, status))) + } } } } From cd4473fe4c45a434d993d004116d9ddbb07c9be5 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Sat, 13 Sep 2025 23:46:47 +0000 Subject: [PATCH 10/10] Refactor MCP failures display logic and enhance table rendering --- pkg/cli/logs.go | 27 ++++----------------------- pkg/console/console.go | 1 + 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/pkg/cli/logs.go b/pkg/cli/logs.go index 0b4c3548409..810bab71cc5 100644 --- a/pkg/cli/logs.go +++ b/pkg/cli/logs.go @@ -451,27 +451,16 @@ func DownloadWorkflowLogs(workflowName string, count int, startDate, endDate, ou } displayLogsOverview(workflowRuns) - // Check for and prominently display MCP failures if any are detected - totalMCPFailures := 0 - for _, pr := range processedRuns { - totalMCPFailures += len(pr.MCPFailures) - } - if totalMCPFailures > 0 { - fmt.Printf("\n%s\n", console.FormatErrorMessage(fmt.Sprintf("āš ļø ALERT: %d MCP server failure(s) detected across analyzed runs", totalMCPFailures))) - fmt.Printf("%s\n", console.FormatWarningMessage("MCP server failures can cause workflow execution issues. See detailed report below.")) - } + // Display MCP failures analysis + displayMCPFailuresAnalysis(processedRuns, verbose) // Display tool call report displayToolCallReport(processedRuns, verbose) - - // Display access log analysis - displayAccessLogAnalysis(processedRuns, verbose) - // Display missing tools analysis displayMissingToolsAnalysis(processedRuns, verbose) - // Display MCP failures analysis - displayMCPFailuresAnalysis(processedRuns, verbose) + // Display access log analysis + displayAccessLogAnalysis(processedRuns, verbose) // Generate tool sequence graph if requested if toolGraph { @@ -1784,7 +1773,6 @@ func displayMCPFailuresAnalysis(processedRuns []ProcessedRun, verbose bool) { // Display summary header fmt.Printf("\n%s\n", console.FormatListHeader("šŸ”Œ MCP Server Failures")) - fmt.Printf("%s\n\n", console.FormatListHeader("=====================")) // Convert map to slice for sorting var summaries []*MCPFailureSummary @@ -1831,13 +1819,6 @@ func displayMCPFailuresAnalysis(processedRuns []ProcessedRun, verbose bool) { fmt.Print(console.RenderTable(tableConfig)) - // Display total summary - uniqueServers := len(failureSummary) - fmt.Printf("\nšŸ“Š %s: %d unique MCP servers failed %d times across workflows\n", - console.FormatCountMessage("Total"), - uniqueServers, - totalFailures) - // Verbose mode: Show detailed breakdown by workflow if verbose && totalFailures > 0 { displayDetailedMCPFailuresBreakdown(processedRuns) diff --git a/pkg/console/console.go b/pkg/console/console.go index 290b03b9d6d..1fa146f3fea 100644 --- a/pkg/console/console.go +++ b/pkg/console/console.go @@ -316,6 +316,7 @@ func RenderTable(config TableConfig) string { output.WriteString(renderTableRow(config.TotalRow, colWidths, totalStyle)) output.WriteString("\n") } + output.WriteString("\n") return output.String() }