From 4ac938cce4d3dc7abc4a462fcf50ebc47250d942 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Aug 2026 04:03:58 +0000 Subject: [PATCH 1/9] feat: add log-parser support for behavior-defined engines Add a `log-parser` field to `EngineBehaviorDefinition` that allows behavior-defined engines (crush, opencode, goose, aider) to declare inline JavaScript log parsers. The parser code is written to disk via heredoc and wrapped with `createEngineLogParser` from `log_parser_shared.cjs` to produce normalized events files matching the contract of built-in engine parsers. Changes: - Add `LogParser` field to `EngineBehaviorDefinition` struct - Add `log-parser` property to JSON schema for behaviors - Implement `GetLogParserScriptId()` on `BehaviorDefinedEngine` - Add `buildLogParserWriteStep()` with heredoc safety checks - Wire log-parser write step into `buildBehaviorDefinedSetupSteps()` - Add comprehensive unit tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 4 + pkg/workflow/behavior_defined_engine.go | 70 +++++++ ...behavior_defined_engine_log_parser_test.go | 174 ++++++++++++++++++ pkg/workflow/engine_definition.go | 9 + 4 files changed, 257 insertions(+) create mode 100644 pkg/workflow/behavior_defined_engine_log_parser_test.go diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 58bb0bb0b17..ed9250a6b03 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -13453,6 +13453,10 @@ "harness-script": { "type": "string", "description": "JavaScript source of a Node.js harness that spawns the engine CLI. When set, the script is written to ${RUNNER_TEMP}/gh-aw/actions/_harness.cjs before execution and the engine is launched as: node [args...]. The harness reads process.env.GH_AW_PROMPT for the prompt-file path and, when the AWF firewall is active, process.env.AWF_REFLECT_ENABLED is set to '1' so the harness can read /reflect data to dynamically configure the engine CLI." + }, + "log-parser": { + "type": "string", + "description": "JavaScript source of a log-parser function for the engine. When set, the script is written to ${RUNNER_TEMP}/gh-aw/actions/_log_parser.cjs and used in the post-agent log-parsing step. The script must export a parseLog(logContent) function that returns {markdown, logEntries, mcpFailures, maxTurnsHit}. A createEngineLogParser wrapper from log_parser_shared.cjs is automatically prepended so the author only provides the parsing function body. This enables behavior-defined engines (e.g. crush, opencode, goose, aider) to produce normalized events files like built-in engine parsers." } }, "additionalProperties": false diff --git a/pkg/workflow/behavior_defined_engine.go b/pkg/workflow/behavior_defined_engine.go index 7e8e98506fa..c0d83d029d5 100644 --- a/pkg/workflow/behavior_defined_engine.go +++ b/pkg/workflow/behavior_defined_engine.go @@ -339,6 +339,73 @@ func (e *BehaviorDefinedEngine) GetMCPConfigAdapterFilename() string { return e.mcpConfigAdapterFilename() } +// logParserHeredocDelimiter is the shell heredoc delimiter used when writing +// the log-parser script to disk. +const logParserHeredocDelimiter = "GHAW_LOG_PARSER_SCRIPT_5d2e8b3f_EOF" + +// logParserScriptFilename returns the filename (not path) for the engine's log-parser script. +func (e *BehaviorDefinedEngine) logParserScriptFilename() string { + return e.GetID() + "_log_parser" +} + +// GetLogParserScriptId returns the log-parser script ID for this engine. +// When the behavior definition includes a log-parser field, this returns +// the engine-specific script ID so that generateLogParsing emits a +// "Parse agent logs" step that requires the inline script. +func (e *BehaviorDefinedEngine) GetLogParserScriptId() string { + behavior := e.behavior() + if behavior == nil || behavior.LogParser == "" { + return "" + } + return e.logParserScriptFilename() +} + +// buildLogParserWriteStep generates a GitHub Actions step that writes the +// behavior-defined engine's log-parser script to +// ${RUNNER_TEMP}/gh-aw/actions/_log_parser.cjs so the post-agent +// log-parsing step can require() it. +// +// The raw log-parser JavaScript from the behavior definition is wrapped with a +// createEngineLogParser call from log_parser_shared.cjs so that the author only +// needs to provide a parseLog(logContent) function returning +// {markdown, logEntries, mcpFailures, maxTurnsHit}. +func (e *BehaviorDefinedEngine) buildLogParserWriteStep() GitHubActionStep { + behavior := e.behavior() + if behavior == nil || behavior.LogParser == "" { + return nil + } + // Wrap the user-provided parse function with the createEngineLogParser + // bootstrap so the script conforms to the contract expected by + // log_parser_bootstrap.cjs (runLogParser). + wrappedScript := fmt.Sprintf(`// @ts-check +// Auto-generated log parser for behavior-defined engine %q. +const { createEngineLogParser } = require("./log_parser_shared.cjs"); + +// --- begin engine-provided parse function --- +%s +// --- end engine-provided parse function --- + +const main = createEngineLogParser({ + parserName: %q, + parseFunction: parseLog, + supportsDirectories: false, +}); + +if (typeof module !== "undefined" && module.exports) { + module.exports = { main, parseLog }; +}`, + e.GetDisplayName(), + behavior.LogParser, + e.GetDisplayName(), + ) + return e.buildScriptWriteStep( + "Write "+e.GetDisplayName()+" log parser script", + e.logParserScriptFilename()+".cjs", + wrappedScript, + logParserHeredocDelimiter, + ) +} + func (e *BehaviorDefinedEngine) GetExecutionSteps(workflowData *WorkflowData, logFile string) []GitHubActionStep { behavior := e.behavior() if behavior == nil || behavior.Execution == nil { @@ -363,6 +430,9 @@ func (e *BehaviorDefinedEngine) buildBehaviorDefinedSetupSteps() []GitHubActionS if harnessStep := e.buildHarnessWriteStep(); len(harnessStep) > 0 { steps = append(steps, harnessStep) } + if logParserStep := e.buildLogParserWriteStep(); len(logParserStep) > 0 { + steps = append(steps, logParserStep) + } return steps } diff --git a/pkg/workflow/behavior_defined_engine_log_parser_test.go b/pkg/workflow/behavior_defined_engine_log_parser_test.go new file mode 100644 index 00000000000..09dc5f2cb8d --- /dev/null +++ b/pkg/workflow/behavior_defined_engine_log_parser_test.go @@ -0,0 +1,174 @@ +//go:build !integration + +package workflow + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testLogParserScript = ` +function parseLog(logContent) { + const lines = logContent.split("\\n").filter(l => l.trim()); + return { + markdown: "### Parsed " + lines.length + " lines", + logEntries: lines.map((l, i) => ({ type: "message", index: i, text: l })), + mcpFailures: [], + maxTurnsHit: false, + }; +} +` + +func newLogParserEngineDefinition() *EngineDefinition { + return &EngineDefinition{ + ID: "testparser", + DisplayName: "TestParser", + Description: "A test engine with a log-parser", + Behaviors: &EngineBehaviorDefinition{ + Execution: &EngineExecutionDefinition{ + CommandName: "testparser-cli", + Args: []string{"run"}, + StepName: "Execute TestParser CLI", + }, + LogParser: testLogParserScript, + }, + } +} + +func TestBehaviorDefinedEngineLogParser_GetLogParserScriptId(t *testing.T) { + t.Run("returns script ID when log-parser is set", func(t *testing.T) { + def := newLogParserEngineDefinition() + engine, err := NewBehaviorDefinedEngine(def) + require.NoError(t, err) + assert.Equal(t, "testparser_log_parser", engine.GetLogParserScriptId()) + }) + + t.Run("returns empty when log-parser is not set", func(t *testing.T) { + def := &EngineDefinition{ + ID: "nologparser", + DisplayName: "NoLogParser", + Behaviors: &EngineBehaviorDefinition{ + Execution: &EngineExecutionDefinition{ + CommandName: "nologparser-cli", + StepName: "Execute", + }, + }, + } + engine, err := NewBehaviorDefinedEngine(def) + require.NoError(t, err) + assert.Empty(t, engine.GetLogParserScriptId()) + }) +} + +func TestBehaviorDefinedEngineLogParser_WriteStep(t *testing.T) { + t.Run("write step is included in execution steps", func(t *testing.T) { + def := newLogParserEngineDefinition() + engine, err := NewBehaviorDefinedEngine(def) + require.NoError(t, err) + + workflowData := &WorkflowData{Name: "test"} + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + + // Steps: [log-parser write, execution] + require.GreaterOrEqual(t, len(steps), 2, "should generate log-parser write step and execution step") + + logParserStepContent := strings.Join(steps[0], "\n") + assert.Contains(t, logParserStepContent, "Write TestParser log parser script") + assert.Contains(t, logParserStepContent, "testparser_log_parser.cjs") + }) + + t.Run("wrapped script contains createEngineLogParser", func(t *testing.T) { + def := newLogParserEngineDefinition() + engine, err := NewBehaviorDefinedEngine(def) + require.NoError(t, err) + + workflowData := &WorkflowData{Name: "test"} + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + require.GreaterOrEqual(t, len(steps), 2) + + logParserStepContent := strings.Join(steps[0], "\n") + assert.Contains(t, logParserStepContent, "createEngineLogParser") + assert.Contains(t, logParserStepContent, "parseFunction: parseLog") + assert.Contains(t, logParserStepContent, `parserName: "TestParser"`) + }) + + t.Run("no write step when log-parser is empty", func(t *testing.T) { + def := &EngineDefinition{ + ID: "noparser", + DisplayName: "NoParser", + Behaviors: &EngineBehaviorDefinition{ + Execution: &EngineExecutionDefinition{ + CommandName: "noparser-cli", + StepName: "Execute", + }, + }, + } + engine, err := NewBehaviorDefinedEngine(def) + require.NoError(t, err) + + workflowData := &WorkflowData{Name: "test"} + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + + // Only execution step, no log-parser write step + require.Len(t, steps, 1) + }) +} + +func TestBehaviorDefinedEngineLogParser_WithHarnessAndLogParser(t *testing.T) { + def := &EngineDefinition{ + ID: "fullengine", + DisplayName: "FullEngine", + Behaviors: &EngineBehaviorDefinition{ + Execution: &EngineExecutionDefinition{ + CommandName: "full-cli", + StepName: "Execute FullEngine", + }, + HarnessScript: `const { spawnSync } = require("child_process");`, + LogParser: testLogParserScript, + }, + } + engine, err := NewBehaviorDefinedEngine(def) + require.NoError(t, err) + + workflowData := &WorkflowData{Name: "test"} + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + + // Steps: [harness write, log-parser write, execution] + require.Len(t, steps, 3, "should generate harness write, log-parser write, and execution steps") + + harnessContent := strings.Join(steps[0], "\n") + assert.Contains(t, harnessContent, "harness script") + + logParserContent := strings.Join(steps[1], "\n") + assert.Contains(t, logParserContent, "log parser script") + + execContent := strings.Join(steps[2], "\n") + assert.Contains(t, execContent, "Execute FullEngine") +} + +func TestBehaviorDefinedEngineLogParser_HeredocDelimiterSafety(t *testing.T) { + def := &EngineDefinition{ + ID: "badparser", + DisplayName: "BadParser", + Behaviors: &EngineBehaviorDefinition{ + Execution: &EngineExecutionDefinition{ + CommandName: "bad-cli", + StepName: "Execute", + }, + // Inject the heredoc delimiter into the script + LogParser: "function parseLog() {}\n" + logParserHeredocDelimiter + "\n// oops", + }, + } + engine, err := NewBehaviorDefinedEngine(def) + require.NoError(t, err) + + // GetLogParserScriptId still returns non-empty (it doesn't validate content) + assert.NotEmpty(t, engine.GetLogParserScriptId()) + + // But the write step should be nil due to heredoc safety check + step := engine.buildLogParserWriteStep() + assert.Nil(t, step, "write step should be nil when script contains heredoc delimiter") +} diff --git a/pkg/workflow/engine_definition.go b/pkg/workflow/engine_definition.go index a381ab1a79d..be67c22b9d8 100644 --- a/pkg/workflow/engine_definition.go +++ b/pkg/workflow/engine_definition.go @@ -246,6 +246,15 @@ type EngineBehaviorDefinition struct { // process.env.AWF_REFLECT_ENABLED / the AWF reflect JSON file to dynamically // configure the engine CLI at runtime. HarnessScript string `yaml:"harness-script,omitempty"` + // LogParser is the JavaScript source of a log-parser function for the engine. + // When non-empty, the script is written to + // ${RUNNER_TEMP}/gh-aw/actions/_log_parser.cjs before the post-agent + // log-parsing step runs. The script must export a parseLog(logContent) function + // that returns {markdown, logEntries, mcpFailures, maxTurnsHit} — the same + // contract used by the built-in engine parsers (e.g. parse_claude_log.cjs). + // A createEngineLogParser wrapper is automatically prepended so the author only + // needs to provide the parsing function body. + LogParser string `yaml:"log-parser,omitempty"` } // AuthBinding maps a logical authentication role to a secret name. From 4c11b0ba8b04419a354c735f25ea04049deb5503 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Aug 2026 04:24:02 +0000 Subject: [PATCH 2/9] feat: add log parsers for goose, crush, and opencode engines Implement engine-specific log parsers using the new log-parser behavior field for each behavior-defined engine: - Goose: parses plain-text streaming output with tool call markers - OpenCode: parses JSON-structured log lines from --print-logs output - Crush: parses plain-text output with tool invocation patterns Each parser extracts conversation blocks, tool calls, MCP failures, and max-turns detection, producing formatted step summaries. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../workflows/daily-arxiv-researcher.lock.yml | 222 +++++++++++- .github/workflows/daily-code-metrics.lock.yml | 222 +++++++++++- .../daily-reliability-review.lock.yml | 224 +++++++++++- .../daily-security-red-team.lock.yml | 330 +++++++++++++++++- .../daily-team-evolution-insights.lock.yml | 212 ++++++++++- .../daily-token-consumption-report.lock.yml | 212 ++++++++++- .github/workflows/shared/crush.md | 81 +++++ .github/workflows/shared/goose.md | 76 ++++ .github/workflows/shared/opencode.md | 82 +++++ .github/workflows/smoke-crush.lock.yml | 222 +++++++++++- .github/workflows/smoke-goose.lock.yml | 212 ++++++++++- .github/workflows/smoke-opencode.lock.yml | 224 +++++++++++- 12 files changed, 2310 insertions(+), 9 deletions(-) diff --git a/.github/workflows/daily-arxiv-researcher.lock.yml b/.github/workflows/daily-arxiv-researcher.lock.yml index e582930bc42..facad316574 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":"5ce97056e53115b83a659bdd43ea13e6d4cf82267465e8fc2112c0ae925b638e","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,109 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" + - name: Write Crush log parser script + 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 entries = []; + 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 toolCalls = 0; + let currentRole = null; + let currentText = []; + + function flushEntry() { + if (currentRole && currentText.length > 0) { + entries.push({ type: currentRole, content: currentText.join("\n").trim() }); + } + currentText = []; + } + + 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()); + } + + // Detect tool-related patterns (Crush uses view/edit/bash/grep/ls tools) + if (/^(Tool|Running|Executing|>\s*(view|edit|bash|grep|ls|write|webfetch|websearch))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + toolCalls++; + currentText.push(line); + continue; + } + // Detect model output blocks + 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 { + entries.push({ type: "text", content: line.trim() }); + } + } + flushEntry(); + + const totalLines = lines.filter(l => l.trim()).length; + const assistantBlocks = entries.filter(e => e.type === "assistant").length; + const textLines = entries.filter(e => e.type === "text").length; + + let md = "### Crush Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${totalLines} |\n`; + md += `| Assistant blocks | ${assistantBlocks} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n\n`; + + if (entries.length > 0) { + md += "
Conversation\n\n"; + let shown = 0; + for (const entry of entries) { + if (entry.type === "text") continue; + if (shown >= 50) break; + const label = entry.type === "tool_use" ? "🔧 Tool" : "🤖 Assistant"; + const content = entry.content.length > 500 ? entry.content.slice(0, 500) + "..." : entry.content; + md += `**${label}**\n\`\`\`\n${content}\n\`\`\`\n\n`; + shown++; + } + if (shown === 0 && textLines > 0) { + const preview = entries.filter(e => e.type === "text").slice(0, 50).map(e => e.content).join("\n"); + md += "```\n" + (preview.length > 3000 ? preview.slice(0, 3000) + "\n..." : preview) + "\n```\n"; + } + md += "
\n"; + } + + return { markdown: md, logEntries: entries, 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 +1161,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 +1765,111 @@ 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 entries = []; + 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 toolCalls = 0; + let currentRole = null; + let currentText = []; + + function flushEntry() { + if (currentRole && currentText.length > 0) { + entries.push({ type: currentRole, content: currentText.join("\n").trim() }); + } + currentText = []; + } + + 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()); + } + + // Detect tool-related patterns (Crush uses view/edit/bash/grep/ls tools) + if (/^(Tool|Running|Executing|>\s*(view|edit|bash|grep|ls|write|webfetch|websearch))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + toolCalls++; + currentText.push(line); + continue; + } + // Detect model output blocks + 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 { + entries.push({ type: "text", content: line.trim() }); + } + } + flushEntry(); + + const totalLines = lines.filter(l => l.trim()).length; + const assistantBlocks = entries.filter(e => e.type === "assistant").length; + const textLines = entries.filter(e => e.type === "text").length; + + let md = "### Crush Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${totalLines} |\n`; + md += `| Assistant blocks | ${assistantBlocks} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n\n`; + + if (entries.length > 0) { + md += "
Conversation\n\n"; + let shown = 0; + for (const entry of entries) { + if (entry.type === "text") continue; + if (shown >= 50) break; + const label = entry.type === "tool_use" ? "🔧 Tool" : "🤖 Assistant"; + const content = entry.content.length > 500 ? entry.content.slice(0, 500) + "..." : entry.content; + md += `**${label}**\n\`\`\`\n${content}\n\`\`\`\n\n`; + shown++; + } + if (shown === 0 && textLines > 0) { + const preview = entries.filter(e => e.type === "text").slice(0, 50).map(e => e.content).join("\n"); + md += "```\n" + (preview.length > 3000 ? preview.slice(0, 3000) + "\n..." : preview) + "\n```\n"; + } + md += "
\n"; + } + + return { markdown: md, logEntries: entries, 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..0a5de7415cc 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":"63bd2ce821ae02c5f4fa6434a30b5f85acadf35d2c532eb4617e70651c19f4aa","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,109 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/crush_harness.cjs" + - name: Write Crush log parser script + 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 entries = []; + 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 toolCalls = 0; + let currentRole = null; + let currentText = []; + + function flushEntry() { + if (currentRole && currentText.length > 0) { + entries.push({ type: currentRole, content: currentText.join("\n").trim() }); + } + currentText = []; + } + + 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()); + } + + // Detect tool-related patterns (Crush uses view/edit/bash/grep/ls tools) + if (/^(Tool|Running|Executing|>\s*(view|edit|bash|grep|ls|write|webfetch|websearch))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + toolCalls++; + currentText.push(line); + continue; + } + // Detect model output blocks + 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 { + entries.push({ type: "text", content: line.trim() }); + } + } + flushEntry(); + + const totalLines = lines.filter(l => l.trim()).length; + const assistantBlocks = entries.filter(e => e.type === "assistant").length; + const textLines = entries.filter(e => e.type === "text").length; + + let md = "### Crush Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${totalLines} |\n`; + md += `| Assistant blocks | ${assistantBlocks} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n\n`; + + if (entries.length > 0) { + md += "
Conversation\n\n"; + let shown = 0; + for (const entry of entries) { + if (entry.type === "text") continue; + if (shown >= 50) break; + const label = entry.type === "tool_use" ? "🔧 Tool" : "🤖 Assistant"; + const content = entry.content.length > 500 ? entry.content.slice(0, 500) + "..." : entry.content; + md += `**${label}**\n\`\`\`\n${content}\n\`\`\`\n\n`; + shown++; + } + if (shown === 0 && textLines > 0) { + const preview = entries.filter(e => e.type === "text").slice(0, 50).map(e => e.content).join("\n"); + md += "```\n" + (preview.length > 3000 ? preview.slice(0, 3000) + "\n..." : preview) + "\n```\n"; + } + md += "
\n"; + } + + return { markdown: md, logEntries: entries, 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 +1275,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 +2105,111 @@ 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 entries = []; + 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 toolCalls = 0; + let currentRole = null; + let currentText = []; + + function flushEntry() { + if (currentRole && currentText.length > 0) { + entries.push({ type: currentRole, content: currentText.join("\n").trim() }); + } + currentText = []; + } + + 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()); + } + + // Detect tool-related patterns (Crush uses view/edit/bash/grep/ls tools) + if (/^(Tool|Running|Executing|>\s*(view|edit|bash|grep|ls|write|webfetch|websearch))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + toolCalls++; + currentText.push(line); + continue; + } + // Detect model output blocks + 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 { + entries.push({ type: "text", content: line.trim() }); + } + } + flushEntry(); + + const totalLines = lines.filter(l => l.trim()).length; + const assistantBlocks = entries.filter(e => e.type === "assistant").length; + const textLines = entries.filter(e => e.type === "text").length; + + let md = "### Crush Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${totalLines} |\n`; + md += `| Assistant blocks | ${assistantBlocks} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n\n`; + + if (entries.length > 0) { + md += "
Conversation\n\n"; + let shown = 0; + for (const entry of entries) { + if (entry.type === "text") continue; + if (shown >= 50) break; + const label = entry.type === "tool_use" ? "🔧 Tool" : "🤖 Assistant"; + const content = entry.content.length > 500 ? entry.content.slice(0, 500) + "..." : entry.content; + md += `**${label}**\n\`\`\`\n${content}\n\`\`\`\n\n`; + shown++; + } + if (shown === 0 && textLines > 0) { + const preview = entries.filter(e => e.type === "text").slice(0, 50).map(e => e.content).join("\n"); + md += "```\n" + (preview.length > 3000 ? preview.slice(0, 3000) + "\n..." : preview) + "\n```\n"; + } + md += "
\n"; + } + + return { markdown: md, logEntries: entries, 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..bd122803e72 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":"cc1b45f28a9825625ec9aa698c4e91c1cb45785455717a76c41bda1492483442","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,110 @@ jobs: echo "$BASE_CONFIG" > "$CONFIG" fi chmod 600 "$CONFIG" + - name: Write OpenCode log parser script + 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 entries = []; + 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 toolCalls = 0; + let errors = 0; + + 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()); + } + + // Try to parse JSON log lines (OpenCode --print-logs output) + let parsed = null; + try { + if (line.trim().startsWith("{")) parsed = JSON.parse(line.trim()); + } catch (e) { /* not JSON */ } + + if (parsed) { + const entry = { + type: parsed.type || parsed.msg || "log", + level: parsed.level || "INFO", + service: parsed.service || "", + message: parsed.msg || parsed.message || "", + raw: line + }; + if (/tool[._]call|tool[._]use/i.test(entry.type)) toolCalls++; + if (/error/i.test(entry.level)) errors++; + if (parsed.input_tokens) inputTokens += parsed.input_tokens; + if (parsed.output_tokens) outputTokens += parsed.output_tokens; + entries.push(entry); + } else { + entries.push({ type: "text", message: line.trim(), raw: line }); + } + } + + const jsonEntries = entries.filter(e => e.type !== "text"); + const textEntries = entries.filter(e => e.type === "text"); + + let md = "### OpenCode Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${lines.filter(l => l.trim()).length} |\n`; + md += `| Structured (JSON) entries | ${jsonEntries.length} |\n`; + md += `| Text entries | ${textEntries.length} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| Errors | ${errors} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n`; + if (inputTokens || outputTokens) { + md += `| Input tokens | ${inputTokens} |\n`; + md += `| Output tokens | ${outputTokens} |\n`; + } + md += "\n"; + + if (jsonEntries.length > 0) { + md += "
Structured Log Entries (first 30)\n\n"; + for (const entry of jsonEntries.slice(0, 30)) { + const level = entry.level === "ERROR" ? "❌" : entry.level === "WARN" ? "⚠️" : "ℹ️"; + const msg = entry.message.length > 200 ? entry.message.slice(0, 200) + "..." : entry.message; + md += `${level} \`[${entry.service}]\` **${entry.type}**: ${msg}\n\n`; + } + md += "
\n"; + } + + if (textEntries.length > 0 && jsonEntries.length === 0) { + md += "
Log Output\n\n```\n"; + const preview = textEntries.slice(0, 100).map(e => e.message).join("\n"); + md += preview.length > 3000 ? preview.slice(0, 3000) + "\n..." : preview; + md += "\n```\n
\n"; + } + + return { markdown: md, logEntries: entries, 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 +1054,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 +1754,112 @@ 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 entries = []; + 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 toolCalls = 0; + let errors = 0; + + 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()); + } + + // Try to parse JSON log lines (OpenCode --print-logs output) + let parsed = null; + try { + if (line.trim().startsWith("{")) parsed = JSON.parse(line.trim()); + } catch (e) { /* not JSON */ } + + if (parsed) { + const entry = { + type: parsed.type || parsed.msg || "log", + level: parsed.level || "INFO", + service: parsed.service || "", + message: parsed.msg || parsed.message || "", + raw: line + }; + if (/tool[._]call|tool[._]use/i.test(entry.type)) toolCalls++; + if (/error/i.test(entry.level)) errors++; + if (parsed.input_tokens) inputTokens += parsed.input_tokens; + if (parsed.output_tokens) outputTokens += parsed.output_tokens; + entries.push(entry); + } else { + entries.push({ type: "text", message: line.trim(), raw: line }); + } + } + + const jsonEntries = entries.filter(e => e.type !== "text"); + const textEntries = entries.filter(e => e.type === "text"); + + let md = "### OpenCode Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${lines.filter(l => l.trim()).length} |\n`; + md += `| Structured (JSON) entries | ${jsonEntries.length} |\n`; + md += `| Text entries | ${textEntries.length} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| Errors | ${errors} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n`; + if (inputTokens || outputTokens) { + md += `| Input tokens | ${inputTokens} |\n`; + md += `| Output tokens | ${outputTokens} |\n`; + } + md += "\n"; + + if (jsonEntries.length > 0) { + md += "
Structured Log Entries (first 30)\n\n"; + for (const entry of jsonEntries.slice(0, 30)) { + const level = entry.level === "ERROR" ? "❌" : entry.level === "WARN" ? "⚠️" : "ℹ️"; + const msg = entry.message.length > 200 ? entry.message.slice(0, 200) + "..." : entry.message; + md += `${level} \`[${entry.service}]\` **${entry.type}**: ${msg}\n\n`; + } + md += "
\n"; + } + + if (textEntries.length > 0 && jsonEntries.length === 0) { + md += "
Log Output\n\n```\n"; + const preview = textEntries.slice(0, 100).map(e => e.message).join("\n"); + md += preview.length > 3000 ? preview.slice(0, 3000) + "\n..." : preview; + md += "\n```\n
\n"; + } + + return { markdown: md, logEntries: entries, 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..5b06cc5dd22 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":"a482c662cd3e3fed58fa109f8a84559a54b644a0d937c2dd8d0806382a8a090d","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,110 @@ jobs: echo "$BASE_CONFIG" > "$CONFIG" fi chmod 600 "$CONFIG" + - name: Write OpenCode log parser script + 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 entries = []; + 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 toolCalls = 0; + let errors = 0; + + 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()); + } + + // Try to parse JSON log lines (OpenCode --print-logs output) + let parsed = null; + try { + if (line.trim().startsWith("{")) parsed = JSON.parse(line.trim()); + } catch (e) { /* not JSON */ } + + if (parsed) { + const entry = { + type: parsed.type || parsed.msg || "log", + level: parsed.level || "INFO", + service: parsed.service || "", + message: parsed.msg || parsed.message || "", + raw: line + }; + if (/tool[._]call|tool[._]use/i.test(entry.type)) toolCalls++; + if (/error/i.test(entry.level)) errors++; + if (parsed.input_tokens) inputTokens += parsed.input_tokens; + if (parsed.output_tokens) outputTokens += parsed.output_tokens; + entries.push(entry); + } else { + entries.push({ type: "text", message: line.trim(), raw: line }); + } + } + + const jsonEntries = entries.filter(e => e.type !== "text"); + const textEntries = entries.filter(e => e.type === "text"); + + let md = "### OpenCode Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${lines.filter(l => l.trim()).length} |\n`; + md += `| Structured (JSON) entries | ${jsonEntries.length} |\n`; + md += `| Text entries | ${textEntries.length} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| Errors | ${errors} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n`; + if (inputTokens || outputTokens) { + md += `| Input tokens | ${inputTokens} |\n`; + md += `| Output tokens | ${outputTokens} |\n`; + } + md += "\n"; + + if (jsonEntries.length > 0) { + md += "
Structured Log Entries (first 30)\n\n"; + for (const entry of jsonEntries.slice(0, 30)) { + const level = entry.level === "ERROR" ? "❌" : entry.level === "WARN" ? "⚠️" : "ℹ️"; + const msg = entry.message.length > 200 ? entry.message.slice(0, 200) + "..." : entry.message; + md += `${level} \`[${entry.service}]\` **${entry.type}**: ${msg}\n\n`; + } + md += "
\n"; + } + + if (textEntries.length > 0 && jsonEntries.length === 0) { + md += "
Log Output\n\n```\n"; + const preview = textEntries.slice(0, 100).map(e => e.message).join("\n"); + md += preview.length > 3000 ? preview.slice(0, 3000) + "\n..." : preview; + md += "\n```\n
\n"; + } + + return { markdown: md, logEntries: entries, 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 +1117,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 +1654,112 @@ 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 entries = []; + 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 toolCalls = 0; + let errors = 0; + + 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()); + } + + // Try to parse JSON log lines (OpenCode --print-logs output) + let parsed = null; + try { + if (line.trim().startsWith("{")) parsed = JSON.parse(line.trim()); + } catch (e) { /* not JSON */ } + + if (parsed) { + const entry = { + type: parsed.type || parsed.msg || "log", + level: parsed.level || "INFO", + service: parsed.service || "", + message: parsed.msg || parsed.message || "", + raw: line + }; + if (/tool[._]call|tool[._]use/i.test(entry.type)) toolCalls++; + if (/error/i.test(entry.level)) errors++; + if (parsed.input_tokens) inputTokens += parsed.input_tokens; + if (parsed.output_tokens) outputTokens += parsed.output_tokens; + entries.push(entry); + } else { + entries.push({ type: "text", message: line.trim(), raw: line }); + } + } + + const jsonEntries = entries.filter(e => e.type !== "text"); + const textEntries = entries.filter(e => e.type === "text"); + + let md = "### OpenCode Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${lines.filter(l => l.trim()).length} |\n`; + md += `| Structured (JSON) entries | ${jsonEntries.length} |\n`; + md += `| Text entries | ${textEntries.length} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| Errors | ${errors} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n`; + if (inputTokens || outputTokens) { + md += `| Input tokens | ${inputTokens} |\n`; + md += `| Output tokens | ${outputTokens} |\n`; + } + md += "\n"; + + if (jsonEntries.length > 0) { + md += "
Structured Log Entries (first 30)\n\n"; + for (const entry of jsonEntries.slice(0, 30)) { + const level = entry.level === "ERROR" ? "❌" : entry.level === "WARN" ? "⚠️" : "ℹ️"; + const msg = entry.message.length > 200 ? entry.message.slice(0, 200) + "..." : entry.message; + md += `${level} \`[${entry.service}]\` **${entry.type}**: ${msg}\n\n`; + } + md += "
\n"; + } + + if (textEntries.length > 0 && jsonEntries.length === 0) { + md += "
Log Output\n\n```\n"; + const preview = textEntries.slice(0, 100).map(e => e.message).join("\n"); + md += preview.length > 3000 ? preview.slice(0, 3000) + "\n..." : preview; + md += "\n```\n
\n"; + } + + return { markdown: md, logEntries: entries, 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 +2002,112 @@ 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 entries = []; + 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 toolCalls = 0; + let errors = 0; + + 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()); + } + + // Try to parse JSON log lines (OpenCode --print-logs output) + let parsed = null; + try { + if (line.trim().startsWith("{")) parsed = JSON.parse(line.trim()); + } catch (e) { /* not JSON */ } + + if (parsed) { + const entry = { + type: parsed.type || parsed.msg || "log", + level: parsed.level || "INFO", + service: parsed.service || "", + message: parsed.msg || parsed.message || "", + raw: line + }; + if (/tool[._]call|tool[._]use/i.test(entry.type)) toolCalls++; + if (/error/i.test(entry.level)) errors++; + if (parsed.input_tokens) inputTokens += parsed.input_tokens; + if (parsed.output_tokens) outputTokens += parsed.output_tokens; + entries.push(entry); + } else { + entries.push({ type: "text", message: line.trim(), raw: line }); + } + } + + const jsonEntries = entries.filter(e => e.type !== "text"); + const textEntries = entries.filter(e => e.type === "text"); + + let md = "### OpenCode Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${lines.filter(l => l.trim()).length} |\n`; + md += `| Structured (JSON) entries | ${jsonEntries.length} |\n`; + md += `| Text entries | ${textEntries.length} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| Errors | ${errors} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n`; + if (inputTokens || outputTokens) { + md += `| Input tokens | ${inputTokens} |\n`; + md += `| Output tokens | ${outputTokens} |\n`; + } + md += "\n"; + + if (jsonEntries.length > 0) { + md += "
Structured Log Entries (first 30)\n\n"; + for (const entry of jsonEntries.slice(0, 30)) { + const level = entry.level === "ERROR" ? "❌" : entry.level === "WARN" ? "⚠️" : "ℹ️"; + const msg = entry.message.length > 200 ? entry.message.slice(0, 200) + "..." : entry.message; + md += `${level} \`[${entry.service}]\` **${entry.type}**: ${msg}\n\n`; + } + md += "
\n"; + } + + if (textEntries.length > 0 && jsonEntries.length === 0) { + md += "
Log Output\n\n```\n"; + const preview = textEntries.slice(0, 100).map(e => e.message).join("\n"); + md += preview.length > 3000 ? preview.slice(0, 3000) + "\n..." : preview; + md += "\n```\n
\n"; + } + + return { markdown: md, logEntries: entries, 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..b2a1f3bd42c 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":"adcf5cc120538e0182ec467fc91d142e354571072f4734de8e0ddbbd2ac62eb3","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,104 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/goose_harness.cjs" + - name: Write Goose log parser script + 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 entries = []; + const mcpFailures = []; + let maxTurnsHit = false; + 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) { + entries.push({ type: currentRole, content: currentText.join("\n").trim() }); + } + currentText = []; + } + + 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()); + } + + // Detect tool use blocks (Goose marks them with ─ or tool indicators) + if (/^─{3,}|^━{3,}/.test(line)) { + flushEntry(); + currentRole = "separator"; + 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()) || /^\s*>\s/.test(line)) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (/^(user|human)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "user") { flushEntry(); currentRole = "user"; } + currentText.push(line); + continue; + } + currentText.push(line); + } + flushEntry(); + + const totalLines = lines.filter(l => l.trim()).length; + const toolCalls = entries.filter(e => e.type === "tool_use").length; + const assistantBlocks = entries.filter(e => e.type === "assistant").length; + + let md = "### Goose Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${totalLines} |\n`; + md += `| Assistant blocks | ${assistantBlocks} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n\n`; + + if (entries.length > 0) { + md += "
Conversation\n\n"; + for (const entry of entries.slice(0, 50)) { + if (entry.type === "separator") continue; + const label = entry.type === "tool_use" ? "🔧 Tool" : entry.type === "assistant" ? "🤖 Assistant" : "👤 User"; + const content = entry.content.length > 500 ? entry.content.slice(0, 500) + "..." : entry.content; + md += `**${label}**\n\`\`\`\n${content}\n\`\`\`\n\n`; + } + md += "
\n"; + } + + return { markdown: md, logEntries: entries, 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 +1187,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 +1771,106 @@ 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 entries = []; + const mcpFailures = []; + let maxTurnsHit = false; + 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) { + entries.push({ type: currentRole, content: currentText.join("\n").trim() }); + } + currentText = []; + } + + 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()); + } + + // Detect tool use blocks (Goose marks them with ─ or tool indicators) + if (/^─{3,}|^━{3,}/.test(line)) { + flushEntry(); + currentRole = "separator"; + 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()) || /^\s*>\s/.test(line)) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (/^(user|human)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "user") { flushEntry(); currentRole = "user"; } + currentText.push(line); + continue; + } + currentText.push(line); + } + flushEntry(); + + const totalLines = lines.filter(l => l.trim()).length; + const toolCalls = entries.filter(e => e.type === "tool_use").length; + const assistantBlocks = entries.filter(e => e.type === "assistant").length; + + let md = "### Goose Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${totalLines} |\n`; + md += `| Assistant blocks | ${assistantBlocks} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n\n`; + + if (entries.length > 0) { + md += "
Conversation\n\n"; + for (const entry of entries.slice(0, 50)) { + if (entry.type === "separator") continue; + const label = entry.type === "tool_use" ? "🔧 Tool" : entry.type === "assistant" ? "🤖 Assistant" : "👤 User"; + const content = entry.content.length > 500 ? entry.content.slice(0, 500) + "..." : entry.content; + md += `**${label}**\n\`\`\`\n${content}\n\`\`\`\n\n`; + } + md += "
\n"; + } + + return { markdown: md, logEntries: entries, 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..3f000cb362b 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":"4b496cfac32736231d271b98a1af186e1f60328da4754812526403a6abe824d6","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,104 @@ jobs: GHAW_HARNESS_SCRIPT_3c7b9f1a_EOF chmod 755 "${RUNNER_TEMP}/gh-aw/actions/goose_harness.cjs" + - name: Write Goose log parser script + 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 entries = []; + const mcpFailures = []; + let maxTurnsHit = false; + 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) { + entries.push({ type: currentRole, content: currentText.join("\n").trim() }); + } + currentText = []; + } + + 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()); + } + + // Detect tool use blocks (Goose marks them with ─ or tool indicators) + if (/^─{3,}|^━{3,}/.test(line)) { + flushEntry(); + currentRole = "separator"; + 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()) || /^\s*>\s/.test(line)) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (/^(user|human)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "user") { flushEntry(); currentRole = "user"; } + currentText.push(line); + continue; + } + currentText.push(line); + } + flushEntry(); + + const totalLines = lines.filter(l => l.trim()).length; + const toolCalls = entries.filter(e => e.type === "tool_use").length; + const assistantBlocks = entries.filter(e => e.type === "assistant").length; + + let md = "### Goose Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${totalLines} |\n`; + md += `| Assistant blocks | ${assistantBlocks} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n\n`; + + if (entries.length > 0) { + md += "
Conversation\n\n"; + for (const entry of entries.slice(0, 50)) { + if (entry.type === "separator") continue; + const label = entry.type === "tool_use" ? "🔧 Tool" : entry.type === "assistant" ? "🤖 Assistant" : "👤 User"; + const content = entry.content.length > 500 ? entry.content.slice(0, 500) + "..." : entry.content; + md += `**${label}**\n\`\`\`\n${content}\n\`\`\`\n\n`; + } + md += "
\n"; + } + + return { markdown: md, logEntries: entries, 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 +1227,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 +1991,106 @@ 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 entries = []; + const mcpFailures = []; + let maxTurnsHit = false; + 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) { + entries.push({ type: currentRole, content: currentText.join("\n").trim() }); + } + currentText = []; + } + + 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()); + } + + // Detect tool use blocks (Goose marks them with ─ or tool indicators) + if (/^─{3,}|^━{3,}/.test(line)) { + flushEntry(); + currentRole = "separator"; + 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()) || /^\s*>\s/.test(line)) { + if (currentRole !== "assistant") { flushEntry(); currentRole = "assistant"; } + currentText.push(line); + continue; + } + if (/^(user|human)\s*[>:]/i.test(line.trim())) { + if (currentRole !== "user") { flushEntry(); currentRole = "user"; } + currentText.push(line); + continue; + } + currentText.push(line); + } + flushEntry(); + + const totalLines = lines.filter(l => l.trim()).length; + const toolCalls = entries.filter(e => e.type === "tool_use").length; + const assistantBlocks = entries.filter(e => e.type === "assistant").length; + + let md = "### Goose Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${totalLines} |\n`; + md += `| Assistant blocks | ${assistantBlocks} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n\n`; + + if (entries.length > 0) { + md += "
Conversation\n\n"; + for (const entry of entries.slice(0, 50)) { + if (entry.type === "separator") continue; + const label = entry.type === "tool_use" ? "🔧 Tool" : entry.type === "assistant" ? "🤖 Assistant" : "👤 User"; + const content = entry.content.length > 500 ? entry.content.slice(0, 500) + "..." : entry.content; + md += `**${label}**\n\`\`\`\n${content}\n\`\`\`\n\n`; + } + md += "
\n"; + } + + return { markdown: md, logEntries: entries, 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..a5432716bdc 100644 --- a/.github/workflows/shared/crush.md +++ b/.github/workflows/shared/crush.md @@ -232,6 +232,87 @@ engine: log(error instanceof Error ? error.message : String(error)); process.exitCode = 1; }); + log-parser: | + function parseLog(logContent) { + const lines = logContent.split("\n"); + const entries = []; + 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 toolCalls = 0; + let currentRole = null; + let currentText = []; + + function flushEntry() { + if (currentRole && currentText.length > 0) { + entries.push({ type: currentRole, content: currentText.join("\n").trim() }); + } + currentText = []; + } + + 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()); + } + + // Detect tool-related patterns (Crush uses view/edit/bash/grep/ls tools) + if (/^(Tool|Running|Executing|>\s*(view|edit|bash|grep|ls|write|webfetch|websearch))\b/i.test(line.trim())) { + flushEntry(); + currentRole = "tool_use"; + toolCalls++; + currentText.push(line); + continue; + } + // Detect model output blocks + 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 { + entries.push({ type: "text", content: line.trim() }); + } + } + flushEntry(); + + const totalLines = lines.filter(l => l.trim()).length; + const assistantBlocks = entries.filter(e => e.type === "assistant").length; + const textLines = entries.filter(e => e.type === "text").length; + + let md = "### Crush Agent Log Summary\n\n"; + md += `| Metric | Value |\n|--------|-------|\n`; + md += `| Total lines | ${totalLines} |\n`; + md += `| Assistant blocks | ${assistantBlocks} |\n`; + md += `| Tool calls | ${toolCalls} |\n`; + md += `| MCP failures | ${mcpFailures.length} |\n`; + md += `| Max turns hit | ${maxTurnsHit} |\n\n`; + + if (entries.length > 0) { + md += "
Conversation\n\n"; + let shown = 0; + for (const entry of entries) { + if (entry.type === "text") continue; + if (shown >= 50) break; + const label = entry.type === "tool_use" ? "🔧 Tool" : "🤖 Assistant"; + const content = entry.content.length > 500 ? entry.content.slice(0, 500) + "..." : entry.content; + md += `**${label}**\n\`\`\`\n${content}\n\`\`\`\n\n`; + shown++; + } + if (shown === 0 && textLines > 0) { + const preview = entries.filter(e => e.type === "text").slice(0, 50).map(e => e.content).join("\n"); + md += "```\n" + (preview.length > 3000 ? preview.slice(0, 3000) + "\n..." : preview) + "\n```\n"; + } + md += "
\n"; + } + + return { markdown: md, logEntries: entries, mcpFailures, maxTurnsHit }; + } ---