diff --git a/.changeset/minor-add-behavior-defined-engine-log-parser.md b/.changeset/minor-add-behavior-defined-engine-log-parser.md new file mode 100644 index 00000000000..941e7b9d0a5 --- /dev/null +++ b/.changeset/minor-add-behavior-defined-engine-log-parser.md @@ -0,0 +1,5 @@ +--- +"gh-aw": minor +--- + +Add `log-parser` behavior field for declarative engines (goose, opencode, crush, aider). Authors define a `parseLog(logContent)` function in YAML and the compiler wraps it with the `createEngineLogParser` bootstrap, producing normalized agent event data used by the standard rendering helpers. The write step runs with `if: always()` so logs are captured even when an earlier step fails. diff --git a/.github/workflows/daily-arxiv-researcher.lock.yml b/.github/workflows/daily-arxiv-researcher.lock.yml index e582930bc42..dec5447a58b 100644 --- a/.github/workflows/daily-arxiv-researcher.lock.yml +++ b/.github/workflows/daily-arxiv-researcher.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"70011987d7384bb4463a7713721ef9a43d7d9f47a80a3bedfc80570e11105d4a","body_hash":"6508cb616d860a827b4defdf94bd2f1a63d59c2785827b5fcd8f8e8c0c4719b9","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fc354c63a31d94df187ac20f7b2f0a2a363268e44bbd37015fac15e4fc8765b9","body_hash":"6508cb616d860a827b4defdf94bd2f1a63d59c2785827b5fcd8f8e8c0c4719b9","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.8","digest":"sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -957,6 +957,95 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" + - name: Write Crush log parser script + if: always() + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/crush_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "Crush". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check|crush-harness)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + let toolCallIndex = 0; + let turnCount = 0; + let currentRole = null; + let currentText = []; + + function flushEntry() { + if (!currentRole || currentText.length === 0) { currentText = []; return; } + const text = currentText.join("\n").trim(); + if (!text) { currentText = []; return; } + if (currentRole === "tool_use") { + const toolId = `crush_tool_${toolCallIndex++}`; + const nameMatch = text.match(/^(?:Tool|Running|Executing|>\s*)(view|edit|bash|grep|ls|write|webfetch|websearch)\b/i); + const toolName = nameMatch ? nameMatch[1] : "unknown_tool"; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: text }] } }); + } else if (currentRole === "assistant") { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + currentText = []; + } + + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (!line.trim()) continue; + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + if (/^(Tool|Running|Executing|>\s*(view|edit|bash|grep|ls|write|webfetch|websearch))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + currentText.push(line); + continue; + } + if (/^(Assistant|Response|Output)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (currentRole) { + currentText.push(line); + } else { + currentText.push(line); + currentRole = "assistant"; + } + } + flushEntry(); + + logEntries.push({ type: "result", num_turns: turnCount, usage: {} }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "Crush", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/crush_log_parser.cjs" - name: Execute Crush CLI id: agentic_execution timeout-minutes: 20 @@ -1058,6 +1147,18 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/crush_log_parser.cjs'); + await main(); - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway @@ -1650,6 +1751,96 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" + - name: Write Crush log parser script + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/crush_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "Crush". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check|crush-harness)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + let toolCallIndex = 0; + let turnCount = 0; + let currentRole = null; + let currentText = []; + + function flushEntry() { + if (!currentRole || currentText.length === 0) { currentText = []; return; } + const text = currentText.join("\n").trim(); + if (!text) { currentText = []; return; } + if (currentRole === "tool_use") { + const toolId = `crush_tool_${toolCallIndex++}`; + const nameMatch = text.match(/^(?:Tool|Running|Executing|>\s*)(view|edit|bash|grep|ls|write|webfetch|websearch)\b/i); + const toolName = nameMatch ? nameMatch[1] : "unknown_tool"; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: text }] } }); + } else if (currentRole === "assistant") { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + currentText = []; + } + + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (!line.trim()) continue; + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + if (/^(Tool|Running|Executing|>\s*(view|edit|bash|grep|ls|write|webfetch|websearch))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + currentText.push(line); + continue; + } + if (/^(Assistant|Response|Output)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (currentRole) { + currentText.push(line); + } else { + currentText.push(line); + currentRole = "assistant"; + } + } + flushEntry(); + + logEntries.push({ type: "result", num_turns: turnCount, usage: {} }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "Crush", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/crush_log_parser.cjs" - name: Execute Crush CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index 5b367406fd9..ee75edeebd3 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"95506e99073b2e44562c08fec9cc4780f63a7d104bc9e31643bc480f71cea0ec","body_hash":"5beb96e50b82d0eef7730bad0cc8f65a316d8162119807c074c5917b1008b94e","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"218ae7ef0b5cec9779c8bc0bc07e0223a1ea504eba0abcfc4d4306dd34029f49","body_hash":"5beb96e50b82d0eef7730bad0cc8f65a316d8162119807c074c5917b1008b94e","strict":true,"agent_id":"crush","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"crush":"0.88.0"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/setup-python","sha":"5fda3b95a4ea91299a34e894583c3862153e4b97","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.8","digest":"sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -1068,6 +1068,95 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" + - name: Write Crush log parser script + if: always() + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/crush_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "Crush". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check|crush-harness)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + let toolCallIndex = 0; + let turnCount = 0; + let currentRole = null; + let currentText = []; + + function flushEntry() { + if (!currentRole || currentText.length === 0) { currentText = []; return; } + const text = currentText.join("\n").trim(); + if (!text) { currentText = []; return; } + if (currentRole === "tool_use") { + const toolId = `crush_tool_${toolCallIndex++}`; + const nameMatch = text.match(/^(?:Tool|Running|Executing|>\s*)(view|edit|bash|grep|ls|write|webfetch|websearch)\b/i); + const toolName = nameMatch ? nameMatch[1] : "unknown_tool"; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: text }] } }); + } else if (currentRole === "assistant") { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + currentText = []; + } + + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (!line.trim()) continue; + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + if (/^(Tool|Running|Executing|>\s*(view|edit|bash|grep|ls|write|webfetch|websearch))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + currentText.push(line); + continue; + } + if (/^(Assistant|Response|Output)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (currentRole) { + currentText.push(line); + } else { + currentText.push(line); + currentRole = "assistant"; + } + } + flushEntry(); + + logEntries.push({ type: "result", num_turns: turnCount, usage: {} }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "Crush", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/crush_log_parser.cjs" - name: Execute Crush CLI id: agentic_execution timeout-minutes: 30 @@ -1172,6 +1261,18 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/crush_log_parser.cjs'); + await main(); - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway @@ -1990,6 +2091,96 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" + - name: Write Crush log parser script + if: always() + continue-on-error: true + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/crush_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "Crush". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check|crush-harness)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + let toolCallIndex = 0; + let turnCount = 0; + let currentRole = null; + let currentText = []; + + function flushEntry() { + if (!currentRole || currentText.length === 0) { currentText = []; return; } + const text = currentText.join("\n").trim(); + if (!text) { currentText = []; return; } + if (currentRole === "tool_use") { + const toolId = `crush_tool_${toolCallIndex++}`; + const nameMatch = text.match(/^(?:Tool|Running|Executing|>\s*)(view|edit|bash|grep|ls|write|webfetch|websearch)\b/i); + const toolName = nameMatch ? nameMatch[1] : "unknown_tool"; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: text }] } }); + } else if (currentRole === "assistant") { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + currentText = []; + } + + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (!line.trim()) continue; + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + if (/^(Tool|Running|Executing|>\s*(view|edit|bash|grep|ls|write|webfetch|websearch))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + currentText.push(line); + continue; + } + if (/^(Assistant|Response|Output)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (currentRole) { + currentText.push(line); + } else { + currentText.push(line); + currentRole = "assistant"; + } + } + flushEntry(); + + logEntries.push({ type: "result", num_turns: turnCount, usage: {} }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "Crush", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/crush_log_parser.cjs" - name: Execute Crush CLI if: always() continue-on-error: true diff --git a/.github/workflows/daily-reliability-review.lock.yml b/.github/workflows/daily-reliability-review.lock.yml index db56b68efa4..012a24a6053 100644 --- a/.github/workflows/daily-reliability-review.lock.yml +++ b/.github/workflows/daily-reliability-review.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4a1ae77a3b62101c341971af41beb4500e228977075522a3b5e025128c131c82","body_hash":"e675f309b20c3dd46f018b974e3a3efc9e9f638895bf05754bfb63d6d743fec4","strict":true,"agent_id":"opencode","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"opencode":"1.2.14"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7135a150fb1de6979cbb00a4c87514776fe534a07dc30ddd053e194eb72b75e0","body_hash":"e675f309b20c3dd46f018b974e3a3efc9e9f638895bf05754bfb63d6d743fec4","strict":true,"agent_id":"opencode","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"opencode":"1.2.14"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.8","digest":"sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -849,6 +849,99 @@ jobs: echo "$BASE_CONFIG" > "$CONFIG" fi chmod 600 "$CONFIG" + - name: Write OpenCode log parser script + if: always() + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/opencode_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "OpenCode". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + let inputTokens = 0; + let outputTokens = 0; + let toolCallIndex = 0; + let turnCount = 0; + let pendingText = []; + + function flushText() { + if (pendingText.length === 0) return; + const text = pendingText.join("\n").trim(); + if (text) { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + pendingText = []; + } + + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (!line.trim()) continue; + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + let parsed = null; + try { + if (line.trim().startsWith("{")) parsed = JSON.parse(line.trim()); + } catch (e) { /* not JSON */ } + + if (parsed) { + const entryType = parsed.type != null ? String(parsed.type) : "log"; + const msg = parsed.msg || parsed.message || ""; + if (parsed.input_tokens) inputTokens += parsed.input_tokens; + if (parsed.output_tokens) outputTokens += parsed.output_tokens; + + if (/tool[._]call|tool[._]use/i.test(entryType)) { + flushText(); + const toolId = `opencode_tool_${toolCallIndex++}`; + const toolName = parsed.tool || parsed.name || entryType; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: msg }] } }); + } else if (msg) { + pendingText.push(msg); + } + } else { + pendingText.push(line.trim()); + } + } + flushText(); + + const usage = {}; + if (inputTokens) usage.input_tokens = inputTokens; + if (outputTokens) usage.output_tokens = outputTokens; + logEntries.push({ type: "result", num_turns: turnCount, usage }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (inputTokens || outputTokens) parts.push(`**Tokens:** ${((inputTokens ?? 0) + (outputTokens ?? 0)).toLocaleString()}`); + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "OpenCode", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/opencode_log_parser.cjs" - name: Execute OpenCode CLI id: agentic_execution timeout-minutes: 30 @@ -950,6 +1043,18 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/opencode_log_parser.cjs'); + await main(); - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway @@ -1638,6 +1743,100 @@ jobs: echo "$BASE_CONFIG" > "$CONFIG" fi chmod 600 "$CONFIG" + - name: Write OpenCode log parser script + if: always() + continue-on-error: true + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/opencode_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "OpenCode". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + let inputTokens = 0; + let outputTokens = 0; + let toolCallIndex = 0; + let turnCount = 0; + let pendingText = []; + + function flushText() { + if (pendingText.length === 0) return; + const text = pendingText.join("\n").trim(); + if (text) { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + pendingText = []; + } + + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (!line.trim()) continue; + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + let parsed = null; + try { + if (line.trim().startsWith("{")) parsed = JSON.parse(line.trim()); + } catch (e) { /* not JSON */ } + + if (parsed) { + const entryType = parsed.type != null ? String(parsed.type) : "log"; + const msg = parsed.msg || parsed.message || ""; + if (parsed.input_tokens) inputTokens += parsed.input_tokens; + if (parsed.output_tokens) outputTokens += parsed.output_tokens; + + if (/tool[._]call|tool[._]use/i.test(entryType)) { + flushText(); + const toolId = `opencode_tool_${toolCallIndex++}`; + const toolName = parsed.tool || parsed.name || entryType; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: msg }] } }); + } else if (msg) { + pendingText.push(msg); + } + } else { + pendingText.push(line.trim()); + } + } + flushText(); + + const usage = {}; + if (inputTokens) usage.input_tokens = inputTokens; + if (outputTokens) usage.output_tokens = outputTokens; + logEntries.push({ type: "result", num_turns: turnCount, usage }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (inputTokens || outputTokens) parts.push(`**Tokens:** ${((inputTokens ?? 0) + (outputTokens ?? 0)).toLocaleString()}`); + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "OpenCode", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/opencode_log_parser.cjs" - name: Execute OpenCode CLI if: always() continue-on-error: true diff --git a/.github/workflows/daily-security-red-team.lock.yml b/.github/workflows/daily-security-red-team.lock.yml index 069c3b7f4b8..50d823ebda6 100644 --- a/.github/workflows/daily-security-red-team.lock.yml +++ b/.github/workflows/daily-security-red-team.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"31dd0cddac1c1bff15aff4b30cb232a0f9feee39b94e712fd473c77bd5851d2d","body_hash":"00d56ccf9f345703965a3d5fa389f4c6e881ef5729a29bae454041d89854fae5","strict":true,"agent_id":"opencode","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"opencode":"1.2.14"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"094531f5b267abb8d0cedf8ce0b8067fad7401bbe1d149e4be98f56c17c6ffad","body_hash":"00d56ccf9f345703965a3d5fa389f4c6e881ef5729a29bae454041d89854fae5","strict":true,"agent_id":"opencode","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"opencode":"1.2.14"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.8","digest":"sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -914,6 +914,99 @@ jobs: echo "$BASE_CONFIG" > "$CONFIG" fi chmod 600 "$CONFIG" + - name: Write OpenCode log parser script + if: always() + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/opencode_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "OpenCode". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + let inputTokens = 0; + let outputTokens = 0; + let toolCallIndex = 0; + let turnCount = 0; + let pendingText = []; + + function flushText() { + if (pendingText.length === 0) return; + const text = pendingText.join("\n").trim(); + if (text) { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + pendingText = []; + } + + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (!line.trim()) continue; + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + let parsed = null; + try { + if (line.trim().startsWith("{")) parsed = JSON.parse(line.trim()); + } catch (e) { /* not JSON */ } + + if (parsed) { + const entryType = parsed.type != null ? String(parsed.type) : "log"; + const msg = parsed.msg || parsed.message || ""; + if (parsed.input_tokens) inputTokens += parsed.input_tokens; + if (parsed.output_tokens) outputTokens += parsed.output_tokens; + + if (/tool[._]call|tool[._]use/i.test(entryType)) { + flushText(); + const toolId = `opencode_tool_${toolCallIndex++}`; + const toolName = parsed.tool || parsed.name || entryType; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: msg }] } }); + } else if (msg) { + pendingText.push(msg); + } + } else { + pendingText.push(line.trim()); + } + } + flushText(); + + const usage = {}; + if (inputTokens) usage.input_tokens = inputTokens; + if (outputTokens) usage.output_tokens = outputTokens; + logEntries.push({ type: "result", num_turns: turnCount, usage }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (inputTokens || outputTokens) parts.push(`**Tokens:** ${((inputTokens ?? 0) + (outputTokens ?? 0)).toLocaleString()}`); + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "OpenCode", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/opencode_log_parser.cjs" - name: Execute OpenCode CLI id: agentic_execution timeout-minutes: 60 @@ -1013,6 +1106,18 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/opencode_log_parser.cjs'); + await main(); - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway @@ -1538,6 +1643,100 @@ jobs: echo "$BASE_CONFIG" > "$CONFIG" fi chmod 600 "$CONFIG" + - name: Write OpenCode log parser script + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/opencode_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "OpenCode". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + let inputTokens = 0; + let outputTokens = 0; + let toolCallIndex = 0; + let turnCount = 0; + let pendingText = []; + + function flushText() { + if (pendingText.length === 0) return; + const text = pendingText.join("\n").trim(); + if (text) { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + pendingText = []; + } + + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (!line.trim()) continue; + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + let parsed = null; + try { + if (line.trim().startsWith("{")) parsed = JSON.parse(line.trim()); + } catch (e) { /* not JSON */ } + + if (parsed) { + const entryType = parsed.type != null ? String(parsed.type) : "log"; + const msg = parsed.msg || parsed.message || ""; + if (parsed.input_tokens) inputTokens += parsed.input_tokens; + if (parsed.output_tokens) outputTokens += parsed.output_tokens; + + if (/tool[._]call|tool[._]use/i.test(entryType)) { + flushText(); + const toolId = `opencode_tool_${toolCallIndex++}`; + const toolName = parsed.tool || parsed.name || entryType; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: msg }] } }); + } else if (msg) { + pendingText.push(msg); + } + } else { + pendingText.push(line.trim()); + } + } + flushText(); + + const usage = {}; + if (inputTokens) usage.input_tokens = inputTokens; + if (outputTokens) usage.output_tokens = outputTokens; + logEntries.push({ type: "result", num_turns: turnCount, usage }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (inputTokens || outputTokens) parts.push(`**Tokens:** ${((inputTokens ?? 0) + (outputTokens ?? 0)).toLocaleString()}`); + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "OpenCode", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/opencode_log_parser.cjs" - name: Execute OpenCode CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true @@ -1780,6 +1979,100 @@ jobs: echo "$BASE_CONFIG" > "$CONFIG" fi chmod 600 "$CONFIG" + - name: Write OpenCode log parser script + if: always() + continue-on-error: true + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/opencode_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "OpenCode". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + let inputTokens = 0; + let outputTokens = 0; + let toolCallIndex = 0; + let turnCount = 0; + let pendingText = []; + + function flushText() { + if (pendingText.length === 0) return; + const text = pendingText.join("\n").trim(); + if (text) { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + pendingText = []; + } + + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (!line.trim()) continue; + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + let parsed = null; + try { + if (line.trim().startsWith("{")) parsed = JSON.parse(line.trim()); + } catch (e) { /* not JSON */ } + + if (parsed) { + const entryType = parsed.type != null ? String(parsed.type) : "log"; + const msg = parsed.msg || parsed.message || ""; + if (parsed.input_tokens) inputTokens += parsed.input_tokens; + if (parsed.output_tokens) outputTokens += parsed.output_tokens; + + if (/tool[._]call|tool[._]use/i.test(entryType)) { + flushText(); + const toolId = `opencode_tool_${toolCallIndex++}`; + const toolName = parsed.tool || parsed.name || entryType; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: msg }] } }); + } else if (msg) { + pendingText.push(msg); + } + } else { + pendingText.push(line.trim()); + } + } + flushText(); + + const usage = {}; + if (inputTokens) usage.input_tokens = inputTokens; + if (outputTokens) usage.output_tokens = outputTokens; + logEntries.push({ type: "result", num_turns: turnCount, usage }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (inputTokens || outputTokens) parts.push(`**Tokens:** ${((inputTokens ?? 0) + (outputTokens ?? 0)).toLocaleString()}`); + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "OpenCode", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/opencode_log_parser.cjs" - name: Execute OpenCode CLI if: always() continue-on-error: true diff --git a/.github/workflows/daily-team-evolution-insights.lock.yml b/.github/workflows/daily-team-evolution-insights.lock.yml index 5911bd3d9bb..39bdaff84e2 100644 --- a/.github/workflows/daily-team-evolution-insights.lock.yml +++ b/.github/workflows/daily-team-evolution-insights.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"9153276985a86f8f213c0a8c0c4fb243804bf9979cbee6c95a3b98b8bf3f4fcb","body_hash":"17d35aa5c651e7e337b624e555a6a913d08fad994e4a4405d36748a497179b01","agent_id":"goose","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"goose":"1.45.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7c860b73fcd80e42643409ab21d9382eb636fb63bfac557b48583b2da081092c","body_hash":"17d35aa5c651e7e337b624e555a6a913d08fad994e4a4405d36748a497179b01","agent_id":"goose","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"goose":"1.45.0"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.8","digest":"sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -987,6 +987,100 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/goose_harness.cjs" + - name: Write Goose log parser script + if: always() + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/goose_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "Goose". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + let turnCount = 0; + let toolCallIndex = 0; + let currentRole = null; + let currentText = []; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check|goose-harness)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + + function flushEntry() { + if (!currentRole || currentText.length === 0) { currentText = []; return; } + const text = currentText.join("\n").trim(); + if (!text) { currentText = []; return; } + if (currentRole === "tool_use") { + const toolId = `goose_tool_${toolCallIndex++}`; + const nameMatch = text.match(/(?:calling|using|tool[_\s]*(?:call|use))\s+(\S+)/i); + const toolName = nameMatch ? nameMatch[1] : "unknown_tool"; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: text }] } }); + } else if (currentRole === "assistant") { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + currentText = []; + } + + // Init entry + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + if (/^─{3,}|^━{3,}/.test(line)) { + flushEntry(); + currentRole = null; + continue; + } + if (/^(calling|using|tool[_\s]*(call|use|result))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + currentText.push(line); + continue; + } + if (/^(assistant|goose)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (/^(user|human)\s*[>:]/i.test(line.trim())) { + flushEntry(); + currentRole = null; + continue; + } + if (currentRole) currentText.push(line); + } + flushEntry(); + + logEntries.push({ type: "result", num_turns: turnCount, usage: {} }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "Goose", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/goose_log_parser.cjs" - name: Execute Goose CLI id: agentic_execution timeout-minutes: 90 @@ -1089,6 +1183,18 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/goose_log_parser.cjs'); + await main(); - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway @@ -1661,6 +1767,101 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/goose_harness.cjs" + - name: Write Goose log parser script + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/goose_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "Goose". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + let turnCount = 0; + let toolCallIndex = 0; + let currentRole = null; + let currentText = []; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check|goose-harness)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + + function flushEntry() { + if (!currentRole || currentText.length === 0) { currentText = []; return; } + const text = currentText.join("\n").trim(); + if (!text) { currentText = []; return; } + if (currentRole === "tool_use") { + const toolId = `goose_tool_${toolCallIndex++}`; + const nameMatch = text.match(/(?:calling|using|tool[_\s]*(?:call|use))\s+(\S+)/i); + const toolName = nameMatch ? nameMatch[1] : "unknown_tool"; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: text }] } }); + } else if (currentRole === "assistant") { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + currentText = []; + } + + // Init entry + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + if (/^─{3,}|^━{3,}/.test(line)) { + flushEntry(); + currentRole = null; + continue; + } + if (/^(calling|using|tool[_\s]*(call|use|result))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + currentText.push(line); + continue; + } + if (/^(assistant|goose)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (/^(user|human)\s*[>:]/i.test(line.trim())) { + flushEntry(); + currentRole = null; + continue; + } + if (currentRole) currentText.push(line); + } + flushEntry(); + + logEntries.push({ type: "result", num_turns: turnCount, usage: {} }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "Goose", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/goose_log_parser.cjs" - name: Execute Goose CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true diff --git a/.github/workflows/daily-token-consumption-report.lock.yml b/.github/workflows/daily-token-consumption-report.lock.yml index 63a77e1a3b4..a6d54475c5c 100644 --- a/.github/workflows/daily-token-consumption-report.lock.yml +++ b/.github/workflows/daily-token-consumption-report.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"11ec65556964e6dc7c95d9960c239cad765a0a371e3f5e9bbf157c6fba2733b5","body_hash":"886778f0d87c563f07651e98d34a5e0472059c173c9c9d229c6b66e173bfd511","strict":true,"agent_id":"goose","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"goose":"1.45.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d1622437331fc96b4f0c2fac51d15213d8a4ac9c87740b79d1f160054a55d1c8","body_hash":"886778f0d87c563f07651e98d34a5e0472059c173c9c9d229c6b66e173bfd511","strict":true,"agent_id":"goose","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"goose":"1.45.0"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.8","digest":"sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -1026,6 +1026,100 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/goose_harness.cjs" + - name: Write Goose log parser script + if: always() + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/goose_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "Goose". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + let turnCount = 0; + let toolCallIndex = 0; + let currentRole = null; + let currentText = []; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check|goose-harness)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + + function flushEntry() { + if (!currentRole || currentText.length === 0) { currentText = []; return; } + const text = currentText.join("\n").trim(); + if (!text) { currentText = []; return; } + if (currentRole === "tool_use") { + const toolId = `goose_tool_${toolCallIndex++}`; + const nameMatch = text.match(/(?:calling|using|tool[_\s]*(?:call|use))\s+(\S+)/i); + const toolName = nameMatch ? nameMatch[1] : "unknown_tool"; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: text }] } }); + } else if (currentRole === "assistant") { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + currentText = []; + } + + // Init entry + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + if (/^─{3,}|^━{3,}/.test(line)) { + flushEntry(); + currentRole = null; + continue; + } + if (/^(calling|using|tool[_\s]*(call|use|result))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + currentText.push(line); + continue; + } + if (/^(assistant|goose)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (/^(user|human)\s*[>:]/i.test(line.trim())) { + flushEntry(); + currentRole = null; + continue; + } + if (currentRole) currentText.push(line); + } + flushEntry(); + + logEntries.push({ type: "result", num_turns: turnCount, usage: {} }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "Goose", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/goose_log_parser.cjs" - name: Execute Goose CLI id: agentic_execution timeout-minutes: 30 @@ -1129,6 +1223,18 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/goose_log_parser.cjs'); + await main(); - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway @@ -1881,6 +1987,101 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/goose_harness.cjs" + - name: Write Goose log parser script + if: always() + continue-on-error: true + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/actions" + cat <<'GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF' > "${RUNNER_TEMP}/gh-aw/actions/goose_log_parser.cjs" + // @ts-check + // Auto-generated log parser for behavior-defined engine "Goose". + const { createEngineLogParser } = require("./log_parser_shared.cjs"); + + // --- begin engine-provided parse function --- + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + let turnCount = 0; + let toolCallIndex = 0; + let currentRole = null; + let currentText = []; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check|goose-harness)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + + function flushEntry() { + if (!currentRole || currentText.length === 0) { currentText = []; return; } + const text = currentText.join("\n").trim(); + if (!text) { currentText = []; return; } + if (currentRole === "tool_use") { + const toolId = `goose_tool_${toolCallIndex++}`; + const nameMatch = text.match(/(?:calling|using|tool[_\s]*(?:call|use))\s+(\S+)/i); + const toolName = nameMatch ? nameMatch[1] : "unknown_tool"; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: text }] } }); + } else if (currentRole === "assistant") { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + currentText = []; + } + + // Init entry + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + if (/^─{3,}|^━{3,}/.test(line)) { + flushEntry(); + currentRole = null; + continue; + } + if (/^(calling|using|tool[_\s]*(call|use|result))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + currentText.push(line); + continue; + } + if (/^(assistant|goose)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (/^(user|human)\s*[>:]/i.test(line.trim())) { + flushEntry(); + currentRole = null; + continue; + } + if (currentRole) currentText.push(line); + } + flushEntry(); + + logEntries.push({ type: "result", num_turns: turnCount, usage: {} }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } + + // --- end engine-provided parse function --- + + const main = createEngineLogParser({ + parserName: "Goose", + parseFunction: parseLog, + supportsDirectories: false, + }); + + if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; + } + GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF + chmod 755 "${RUNNER_TEMP}/gh-aw/actions/goose_log_parser.cjs" - name: Execute Goose CLI if: always() continue-on-error: true diff --git a/.github/workflows/shared/crush.md b/.github/workflows/shared/crush.md index 7712938ec6f..59820b46fba 100644 --- a/.github/workflows/shared/crush.md +++ b/.github/workflows/shared/crush.md @@ -232,6 +232,72 @@ engine: log(error instanceof Error ? error.message : String(error)); process.exitCode = 1; }); + log-parser: | + function parseLog(logContent) { + const lines = logContent.split("\n"); + const logEntries = []; + const mcpFailures = []; + let maxTurnsHit = false; + const AWF_INFRA_RE = /^\[(INFO|WARN|SUCCESS|ERROR|entrypoint|health-check|crush-harness)\]|^ (?:Container|Network|Volume) |^Process exiting with code:/; + let toolCallIndex = 0; + let turnCount = 0; + let currentRole = null; + let currentText = []; + + function flushEntry() { + if (!currentRole || currentText.length === 0) { currentText = []; return; } + const text = currentText.join("\n").trim(); + if (!text) { currentText = []; return; } + if (currentRole === "tool_use") { + const toolId = `crush_tool_${toolCallIndex++}`; + const nameMatch = text.match(/^(?:Tool|Running|Executing|>\s*)(view|edit|bash|grep|ls|write|webfetch|websearch)\b/i); + const toolName = nameMatch ? nameMatch[1] : "unknown_tool"; + logEntries.push({ type: "assistant", message: { content: [{ type: "tool_use", id: toolId, name: toolName, input: {} }] } }); + logEntries.push({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: toolId, content: text }] } }); + } else if (currentRole === "assistant") { + logEntries.push({ type: "assistant", message: { content: [{ type: "text", text }] } }); + turnCount++; + } + currentText = []; + } + + logEntries.push({ type: "system", subtype: "init", model: null, session_id: null }); + + for (const line of lines) { + if (!line.trim()) continue; + if (AWF_INFRA_RE.test(line)) continue; + if (/max.?turns|maximum.*turns.*reached|turn limit/i.test(line)) maxTurnsHit = true; + if (/MCP server .* failed|MCP.*connection.*error|Failed to connect to MCP/i.test(line)) { + const serverMatch = line.match(/MCP server ['"]?([^\s'"]+)['"]?/i); + mcpFailures.push(serverMatch ? serverMatch[1] : line.trim()); + } + + if (/^(Tool|Running|Executing|>\s*(view|edit|bash|grep|ls|write|webfetch|websearch))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + currentText.push(line); + continue; + } + if (/^(Assistant|Response|Output)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (currentRole) { + currentText.push(line); + } else { + currentText.push(line); + currentRole = "assistant"; + } + } + flushEntry(); + + logEntries.push({ type: "result", num_turns: turnCount, usage: {} }); + const parts = [`**Turns:** ${turnCount}`, `**Tool calls:** ${toolCallIndex}`]; + if (mcpFailures.length) parts.push(`**MCP failures:** ${mcpFailures.length}`); + if (maxTurnsHit) parts.push("**Max turns reached**"); + return { markdown: parts.join(" · "), logEntries, mcpFailures, maxTurnsHit }; + } ---