From 8754f144a64a5a6da12abca78fa45923f0df9d12 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Jul 2026 07:13:38 +0000
Subject: [PATCH 1/8] Improve step summary progressive disclosure
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.../js/generate_observability_summary.cjs | 2 +-
.../generate_observability_summary.test.cjs | 2 +-
actions/setup/js/log_parser_format.cjs | 160 +++++++++---------
actions/setup/js/log_parser_shared.cjs | 25 ++-
actions/setup/js/log_parser_shared.test.cjs | 15 +-
actions/setup/js/parse_antigravity_log.cjs | 12 +-
.../setup/js/parse_antigravity_log.test.cjs | 4 +-
actions/setup/js/parse_claude_log.cjs | 13 +-
actions/setup/js/parse_claude_log.test.cjs | 23 ++-
actions/setup/js/parse_codex_log.cjs | 28 +--
actions/setup/js/parse_codex_log.test.cjs | 40 +++--
actions/setup/js/parse_copilot_log.cjs | 9 +-
actions/setup/js/parse_copilot_log.test.cjs | 21 ++-
actions/setup/js/parse_custom_log.cjs | 17 +-
actions/setup/js/parse_gemini_log.cjs | 14 +-
actions/setup/js/parse_gemini_log.test.cjs | 14 +-
actions/setup/js/parse_pi_log.cjs | 14 +-
actions/setup/js/parse_pi_log.test.cjs | 4 +-
18 files changed, 227 insertions(+), 190 deletions(-)
diff --git a/actions/setup/js/generate_observability_summary.cjs b/actions/setup/js/generate_observability_summary.cjs
index 12804d3535d..2d217751a6e 100644
--- a/actions/setup/js/generate_observability_summary.cjs
+++ b/actions/setup/js/generate_observability_summary.cjs
@@ -153,7 +153,7 @@ function buildObservabilitySummary(data) {
lines.push(`- **staged**: ${data.staged}`);
if (data.otlpExportErrors > 0) {
- lines.push("- ā ļø OTLP export failures detected; telemetry may not be visible in the backend.");
+ lines.push("- OTLP export failures detected; telemetry may not be visible in the backend.");
}
if (data.createdItemTypes.length > 0) {
diff --git a/actions/setup/js/generate_observability_summary.test.cjs b/actions/setup/js/generate_observability_summary.test.cjs
index 5f1033463b5..d8e9585ebc1 100644
--- a/actions/setup/js/generate_observability_summary.test.cjs
+++ b/actions/setup/js/generate_observability_summary.test.cjs
@@ -74,7 +74,7 @@ describe("generate_observability_summary.cjs", () => {
expect(summary).toContain("- **blocked requests**: 1");
expect(summary).toContain("- **agent output errors**: 1");
expect(summary).toContain("- **otlp export errors**: 2");
- expect(summary).toContain("- ā ļø OTLP export failures detected; telemetry may not be visible in the backend.");
+ expect(summary).toContain("- OTLP export failures detected; telemetry may not be visible in the backend.");
expect(summary).toContain("- **otlp export failure details**:");
expect(summary).toContain(" - collector-a.example.com:4318 status=401 reason=Unauthorized");
expect(summary).toContain(" - collector-b.example.com:4318 reason=upstream timeout");
diff --git a/actions/setup/js/log_parser_format.cjs b/actions/setup/js/log_parser_format.cjs
index 2ae3fa29438..2665a63d89a 100644
--- a/actions/setup/js/log_parser_format.cjs
+++ b/actions/setup/js/log_parser_format.cjs
@@ -87,6 +87,19 @@ function createLogParserFormatters(deps) {
return logEntries;
}
+ /**
+ * Builds a step-summary section with a plain-text h3 heading and collapsible details body.
+ * @param {string} title
+ * @param {string} summary
+ * @param {string} body
+ * @returns {string}
+ */
+ function buildStepSummaryDetailsSection(title, summary, body) {
+ const trimmedBody = typeof body === "string" ? body.trim() : "";
+ const content = trimmedBody || `No ${title.toLowerCase()} available.`;
+ return `### ${title}\n\n\n${summary}
\n\n${content}\n \n\n`;
+ }
+
/**
* Generates markdown summary from conversation log entries
* This is the core shared logic between Claude and Copilot log parsers
@@ -104,7 +117,6 @@ function createLogParserFormatters(deps) {
function generateConversationMarkdown(logEntries, options) {
const { formatToolCallback, formatInitCallback, summaryTracker } = options;
const renderEntries = normalizeEntriesForRendering(logEntries);
-
const toolUsePairs = collectToolUsePairs(renderEntries);
let markdown = "";
@@ -120,117 +132,103 @@ function createLogParserFormatters(deps) {
}
const initEntry = renderEntries.find(entry => entry.type === "system" && entry.subtype === "init");
-
if (initEntry && formatInitCallback) {
- if (!addContent("## š Initialization\n\n")) {
- return { markdown, commandSummary: [], sizeLimitReached };
- }
const initResult = formatInitCallback(initEntry);
- if (typeof initResult === "string") {
- if (!addContent(initResult)) {
- return { markdown, commandSummary: [], sizeLimitReached };
- }
- } else if (initResult && initResult.markdown) {
- if (!addContent(initResult.markdown)) {
- return { markdown, commandSummary: [], sizeLimitReached };
- }
- }
- if (!addContent("\n")) {
+ const initBody = typeof initResult === "string" ? initResult : initResult && initResult.markdown ? initResult.markdown : "";
+ if (!addContent(buildStepSummaryDetailsSection("Initialization", "Show initialization details", initBody))) {
+ markdown += SIZE_LIMIT_WARNING;
return { markdown, commandSummary: [], sizeLimitReached };
}
}
- if (!addContent("\n## š¤ Reasoning\n\n")) {
- return { markdown, commandSummary: [], sizeLimitReached };
- }
+ let reasoningBody = "";
+ let commandDetailsBody = "";
for (const entry of renderEntries) {
- if (sizeLimitReached) break;
-
- if (entry.type === "assistant" && entry.message?.content) {
- for (const content of entry.message.content) {
- if (sizeLimitReached) break;
+ if (entry.type !== "assistant" || !entry.message?.content) {
+ continue;
+ }
- if (content.type === "text" && content.text) {
- let text = content.text.trim();
- text = unfenceMarkdown(text);
- if (text && text.length > 0) {
- if (!addContent(text + "\n\n")) {
- break;
- }
- }
- } else if (content.type === "thinking" && content.thinking) {
- let text = content.thinking.trim();
- text = unfenceMarkdown(text);
- if (text && text.length > 0) {
- if (!addContent(`ā ${text.replace(/\n/g, "
")}\n\n`)) {
- break;
- }
- }
- } else if (content.type === "tool_use") {
- const toolResult = toolUsePairs.get(content.id);
- const toolMarkdown = formatToolCallback(content, toolResult);
- if (toolMarkdown) {
- if (!addContent(toolMarkdown)) {
- break;
- }
- }
+ for (const content of entry.message.content) {
+ if (content.type === "text" && content.text) {
+ let text = content.text.trim();
+ text = unfenceMarkdown(text);
+ if (text) {
+ reasoningBody += text + "\n\n";
+ }
+ } else if (content.type === "thinking" && content.thinking) {
+ let text = content.thinking.trim();
+ text = unfenceMarkdown(text);
+ if (text) {
+ commandDetailsBody += "";
+ reasoningBody += `${text.replace(/\n/g, "
")}\n\n`;
+ }
+ } else if (content.type === "tool_use") {
+ const toolResult = toolUsePairs.get(content.id);
+ const toolMarkdown = formatToolCallback(content, toolResult);
+ if (toolMarkdown) {
+ commandDetailsBody += toolMarkdown;
}
}
}
}
- if (sizeLimitReached) {
+ if (!addContent(buildStepSummaryDetailsSection("Reasoning", "Show reasoning", reasoningBody))) {
markdown += SIZE_LIMIT_WARNING;
return { markdown, commandSummary: [], sizeLimitReached };
}
- if (!addContent("## š¤ Commands and Tools\n\n")) {
+ if (sizeLimitReached) {
markdown += SIZE_LIMIT_WARNING;
- return { markdown, commandSummary: [], sizeLimitReached: true };
+ return { markdown, commandSummary: [], sizeLimitReached };
}
const commandSummary = [];
-
for (const entry of renderEntries) {
- if (entry.type === "assistant" && entry.message?.content) {
- for (const content of entry.message.content) {
- if (content.type === "tool_use") {
- const toolName = content.name;
- const input = content.input || {};
+ if (entry.type !== "assistant" || !entry.message?.content) {
+ continue;
+ }
- if (INTERNAL_TOOLS.includes(toolName)) {
- continue;
- }
+ for (const content of entry.message.content) {
+ if (content.type !== "tool_use") {
+ continue;
+ }
- const toolResult = toolUsePairs.get(content.id);
- let statusIcon = "ā";
- if (toolResult) {
- statusIcon = toolResult.is_error === true ? "ā" : "ā
";
- }
+ const toolName = content.name;
+ const input = content.input || {};
+ if (INTERNAL_TOOLS.includes(toolName)) {
+ continue;
+ }
- if (toolName === "Bash") {
- const formattedCommand = formatBashCommand(input.command || "");
- commandSummary.push(`* ${statusIcon} \`${formattedCommand}\``);
- } else if (toolName.startsWith("mcp__")) {
- const mcpName = formatMcpName(toolName);
- commandSummary.push(`* ${statusIcon} \`${mcpName}(...)\``);
- } else {
- commandSummary.push(`* ${statusIcon} ${toolName}`);
- }
- }
+ const toolResult = toolUsePairs.get(content.id);
+ let statusIcon = "ā";
+ if (toolResult) {
+ statusIcon = toolResult.is_error === true ? "ā" : "ā
";
+ }
+
+ if (toolName === "Bash") {
+ const formattedCommand = formatBashCommand(input.command || "");
+ commandSummary.push(`* ${statusIcon} \`${formattedCommand}\``);
+ } else if (toolName.startsWith("mcp__")) {
+ const mcpName = formatMcpName(toolName);
+ commandSummary.push(`* ${statusIcon} \`${mcpName}(...)\``);
+ } else {
+ commandSummary.push(`* ${statusIcon} ${toolName}`);
}
}
}
+ let commandsBody = "";
if (commandSummary.length > 0) {
- for (const cmd of commandSummary) {
- if (!addContent(`${cmd}\n`)) {
- markdown += SIZE_LIMIT_WARNING;
- return { markdown, commandSummary, sizeLimitReached: true };
- }
- }
- } else if (!addContent("No commands or tools used.\n")) {
+ commandsBody += commandSummary.join("\n") + "\n\n";
+ } else {
+ commandsBody += "No commands or tools used.\n";
+ }
+ if (commandDetailsBody.trim()) {
+ commandsBody += commandDetailsBody.trim() + "\n";
+ }
+
+ if (!addContent(buildStepSummaryDetailsSection("Commands and Tools", "Show commands and tool calls", commandsBody))) {
markdown += SIZE_LIMIT_WARNING;
return { markdown, commandSummary, sizeLimitReached: true };
}
diff --git a/actions/setup/js/log_parser_shared.cjs b/actions/setup/js/log_parser_shared.cjs
index ce18773e3d5..249ade5c705 100644
--- a/actions/setup/js/log_parser_shared.cjs
+++ b/actions/setup/js/log_parser_shared.cjs
@@ -42,7 +42,7 @@ const MAX_AGENT_TEXT_LENGTH = 2000;
* This message is added directly to markdown (not tracked) to ensure it's always visible.
* The message is small (~70 bytes) and won't cause practical issues with the 8MB limit.
*/
-const SIZE_LIMIT_WARNING = "\n\nā ļø *Step summary size limit reached. Additional content truncated.*\n\n";
+const SIZE_LIMIT_WARNING = "\n\n*Step summary size limit reached. Additional content truncated.*\n\n";
/**
* Matches AWF infrastructure lines written by the firewall/container wrapper.
@@ -262,6 +262,22 @@ function isLikelyCustomAgent(toolName) {
return true;
}
+/**
+ * Builds a step-summary section with an h3 heading and collapsible details body.
+ * @param {string} title
+ * @param {string} summary
+ * @param {string} body
+ * @param {{open?: boolean}} [options]
+ * @returns {string}
+ */
+function buildStepSummaryDetailsSection(title, summary, body, options = {}) {
+ const { open = false } = options;
+ const openAttr = open ? " open" : "";
+ const trimmedBody = typeof body === "string" ? body.trim() : "";
+ const content = trimmedBody || `No ${title.toLowerCase()} available.`;
+ return `### ${title}\n\n\n${summary}
\n\n${content}\n \n\n`;
+}
+
/**
* Generates information section markdown from the last log entry
* @param {any} lastEntry - The last log entry with metadata (num_turns, duration_ms, etc.)
@@ -272,10 +288,10 @@ function isLikelyCustomAgent(toolName) {
function generateInformationSection(lastEntry, options = {}) {
const { additionalInfoCallback } = options;
- let markdown = "\n## š Information\n\n";
+ let markdown = "";
if (!lastEntry) {
- return markdown;
+ return buildStepSummaryDetailsSection("Information", "Show run metadata", "");
}
if (lastEntry.num_turns) {
@@ -333,7 +349,7 @@ function generateInformationSection(lastEntry, options = {}) {
markdown += `**Permission Denials:** ${lastEntry.permission_denials.length}\n\n`;
}
- return markdown;
+ return buildStepSummaryDetailsSection("Information", "Show run metadata", markdown);
}
/**
@@ -1372,6 +1388,7 @@ module.exports = {
estimateTokens,
formatMcpName,
isLikelyCustomAgent,
+ buildStepSummaryDetailsSection,
generateConversationMarkdown,
generateInformationSection,
formatMcpParameters,
diff --git a/actions/setup/js/log_parser_shared.test.cjs b/actions/setup/js/log_parser_shared.test.cjs
index 0a87894dce4..8699b32316e 100644
--- a/actions/setup/js/log_parser_shared.test.cjs
+++ b/actions/setup/js/log_parser_shared.test.cjs
@@ -286,12 +286,12 @@ describe("log_parser_shared.cjs", () => {
formatInitCallback,
});
- expect(result.markdown).toContain("## š Initialization");
+ expect(result.markdown).toContain("### Initialization");
expect(result.markdown).toContain("Model: test-model");
- expect(result.markdown).toContain("## š¤ Reasoning");
+ expect(result.markdown).toContain("### Reasoning");
expect(result.markdown).toContain("Let me help with that.");
expect(result.markdown).toContain("Tool: Bash");
- expect(result.markdown).toContain("## š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Commands and Tools");
expect(result.commandSummary).toHaveLength(1);
expect(result.commandSummary[0]).toContain("ā
");
expect(result.commandSummary[0]).toContain("echo hello");
@@ -305,8 +305,8 @@ describe("log_parser_shared.cjs", () => {
formatInitCallback: () => "",
});
- expect(result.markdown).toContain("## š¤ Reasoning");
- expect(result.markdown).toContain("## š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Reasoning");
+ expect(result.markdown).toContain("### Commands and Tools");
expect(result.markdown).toContain("No commands or tools used.");
expect(result.commandSummary).toHaveLength(0);
});
@@ -375,7 +375,7 @@ describe("log_parser_shared.cjs", () => {
const result = generateInformationSection(lastEntry);
- expect(result).toContain("## š Information");
+ expect(result).toContain("### Information");
expect(result).toContain("**Turns:** 5");
expect(result).toContain("**Duration:** 2m 5s");
expect(result).toContain("**Total Cost:** $0.0123");
@@ -475,7 +475,8 @@ describe("log_parser_shared.cjs", () => {
const result = generateInformationSection(null);
- expect(result).toBe("\n## š Information\n\n");
+ expect(result).toContain("### Information");
+ expect(result).toContain("No information available.");
});
});
diff --git a/actions/setup/js/parse_antigravity_log.cjs b/actions/setup/js/parse_antigravity_log.cjs
index cf38e08b240..6e380472f70 100644
--- a/actions/setup/js/parse_antigravity_log.cjs
+++ b/actions/setup/js/parse_antigravity_log.cjs
@@ -1,7 +1,7 @@
// @ts-check
///
-const { createEngineLogParser, generateInformationSection, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
+const { createEngineLogParser, generateInformationSection, buildStepSummaryDetailsSection, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
const main = createEngineLogParser({
parserName: "Antigravity",
@@ -27,7 +27,7 @@ const main = createEngineLogParser({
function parseAntigravityLog(logContent) {
if (!logContent) {
return {
- markdown: "## š¤ Antigravity\n\nNo log content provided.\n\n",
+ markdown: buildStepSummaryDetailsSection("Antigravity", "Show parser status", "No log content provided."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
@@ -53,7 +53,7 @@ function parseAntigravityLog(logContent) {
if (parsedLines.length === 0) {
return {
- markdown: "## š¤ Antigravity\n\nLog format not recognized as Antigravity stream-json.\n\n",
+ markdown: buildStepSummaryDetailsSection("Antigravity", "Show parser status", "Log format not recognized as Antigravity stream-json."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
@@ -66,11 +66,7 @@ function parseAntigravityLog(logContent) {
const stats = lastEntry.stats || {};
// Build markdown output
- let markdown = "## š¤ Antigravity\n\n";
-
- if (finalResponse.trim()) {
- markdown += finalResponse.trim() + "\n\n";
- }
+ let markdown = buildStepSummaryDetailsSection("Antigravity", "Show final response", finalResponse.trim());
// Compute aggregated token usage from all models
let totalInputTokens = 0;
diff --git a/actions/setup/js/parse_antigravity_log.test.cjs b/actions/setup/js/parse_antigravity_log.test.cjs
index ef1b94e7167..ae2b7b10571 100644
--- a/actions/setup/js/parse_antigravity_log.test.cjs
+++ b/actions/setup/js/parse_antigravity_log.test.cjs
@@ -107,7 +107,7 @@ describe("parse_antigravity_log.cjs", () => {
const result = parseAntigravityLog(logContent);
- expect(result.markdown).toContain("## š¤ Antigravity");
+ expect(result.markdown).toContain("### Antigravity");
expect(result.markdown).toContain("Hello from Antigravity");
expect(result.markdown).toContain("500");
expect(result.markdown).toContain("200");
@@ -128,7 +128,7 @@ describe("parse_antigravity_log.cjs", () => {
const result = parseAntigravityLog(logContent);
- expect(result.markdown).toContain("## š¤ Antigravity");
+ expect(result.markdown).toContain("### Antigravity");
expect(result.logEntries).toHaveLength(0);
});
diff --git a/actions/setup/js/parse_claude_log.cjs b/actions/setup/js/parse_claude_log.cjs
index b681975f931..fc3b783783a 100644
--- a/actions/setup/js/parse_claude_log.cjs
+++ b/actions/setup/js/parse_claude_log.cjs
@@ -1,7 +1,16 @@
// @ts-check
///
-const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, parseLogEntries, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
+const {
+ createEngineLogParser,
+ generateConversationMarkdown,
+ generateInformationSection,
+ buildStepSummaryDetailsSection,
+ formatInitializationSummary,
+ formatToolUse,
+ parseLogEntries,
+ convertLegacyLogEntriesToCopilotEvents,
+} = require("./log_parser_shared.cjs");
const main = createEngineLogParser({
parserName: "Claude",
@@ -20,7 +29,7 @@ function parseClaudeLog(logContent) {
if (!logEntries) {
return {
- markdown: "## Agent Log Summary\n\nLog format not recognized as Claude JSON array or JSONL.\n",
+ markdown: buildStepSummaryDetailsSection("Agent Log Summary", "Show parser status", "Log format not recognized as Claude JSON array or JSONL."),
mcpFailures: [],
maxTurnsHit: false,
logEntries: [],
diff --git a/actions/setup/js/parse_claude_log.test.cjs b/actions/setup/js/parse_claude_log.test.cjs
index f7bd3e68451..21412bbc4bc 100644
--- a/actions/setup/js/parse_claude_log.test.cjs
+++ b/actions/setup/js/parse_claude_log.test.cjs
@@ -86,8 +86,8 @@ describe("parse_claude_log.cjs", () => {
]);
const result = parseClaudeLog(jsonArrayLog);
- expect(result.markdown).toContain("š Initialization");
- expect(result.markdown).toContain("š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("### Commands and Tools");
expect(result.markdown).toContain("test-123");
expect(result.markdown).toContain("echo 'Hello World'");
expect(result.markdown).toContain("Total Cost");
@@ -99,8 +99,8 @@ describe("parse_claude_log.cjs", () => {
'[DEBUG] Starting Claude Code CLI\n[ERROR] Some error occurred\nnpm warn exec The following package was not found\n[{"type":"system","subtype":"init","session_id":"29d324d8-1a92-43c6-8740-babc2875a1d6","tools":["Task","Bash","mcp__safe_outputs__missing-tool"],"model":"claude-sonnet-4-20250514"},{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool_123","name":"mcp__safe_outputs__missing-tool","input":{"tool":"draw_pelican","reason":"Tool needed to draw pelican artwork"}}]}},{"type":"result","total_cost_usd":0.1789264,"usage":{"input_tokens":25,"output_tokens":832},"num_turns":10}]\n[DEBUG] Session completed'
);
- expect(result.markdown).toContain("š Initialization");
- expect(result.markdown).toContain("š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("### Commands and Tools");
expect(result.markdown).toContain("29d324d8-1a92-43c6-8740-babc2875a1d6");
expect(result.markdown).toContain("safe_outputs::missing-tool");
expect(result.markdown).toContain("Total Cost");
@@ -112,8 +112,8 @@ describe("parse_claude_log.cjs", () => {
'[DEBUG] Starting Claude Code CLI\n{"type":"system","subtype":"init","session_id":"test-456","tools":["Bash","Read"],"model":"claude-sonnet-4-20250514"}\n[DEBUG] Processing user prompt\n{"type":"assistant","message":{"content":[{"type":"text","text":"I\'ll help you."},{"type":"tool_use","id":"tool_123","name":"Bash","input":{"command":"ls -la"}}]}}\n{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool_123","content":"file1.txt\\nfile2.txt"}]}}\n{"type":"result","total_cost_usd":0.002,"usage":{"input_tokens":100,"output_tokens":25},"num_turns":2}\n[DEBUG] Workflow completed'
);
- expect(result.markdown).toContain("š Initialization");
- expect(result.markdown).toContain("š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("### Commands and Tools");
expect(result.markdown).toContain("test-456");
expect(result.markdown).toContain("ls -la");
expect(result.markdown).toContain("Total Cost");
@@ -136,7 +136,7 @@ describe("parse_claude_log.cjs", () => {
]);
const result = parseClaudeLog(logWithFailures);
- expect(result.markdown).toContain("š Initialization");
+ expect(result.markdown).toContain("### Initialization");
expect(result.markdown).toContain("failed_server (failed)");
expect(result.mcpFailures).toEqual(["failed_server"]);
});
@@ -164,7 +164,7 @@ describe("parse_claude_log.cjs", () => {
]);
const result = parseClaudeLog(logWithDetailedErrors);
- expect(result.markdown).toContain("š Initialization");
+ expect(result.markdown).toContain("### Initialization");
expect(result.markdown).toContain("failed_with_error (failed)");
expect(result.markdown).toContain("**Error:** Connection timeout after 30s");
expect(result.markdown).toContain("**Stderr:**");
@@ -264,7 +264,7 @@ describe("parse_claude_log.cjs", () => {
it("should skip debug lines that look like arrays but aren't JSON", () => {
const result = parseClaudeLog('[DEBUG] Starting\n[INFO] Processing\n[{"type":"system","subtype":"init","session_id":"test","tools":["Bash"],"model":"claude-sonnet-4-20250514"}]\n[DEBUG] Done');
- expect(result.markdown).toContain("š Initialization");
+ expect(result.markdown).toContain("### Initialization");
});
it("should handle tool use with MCP tools", () => {
@@ -568,7 +568,7 @@ describe("parse_claude_log.cjs", () => {
expect(result.markdown).toContain("Grep");
expect(result.markdown).toContain("Bash");
- const toolsSection = result.markdown.split("## š¤ Reasoning")[0];
+ const toolsSection = result.markdown.split("### Reasoning")[0];
expect(toolsSection).not.toMatch(/and \d+ more/);
});
@@ -634,8 +634,7 @@ describe("parse_claude_log.cjs", () => {
]);
const result = parseClaudeLog(logWithThinking);
- // Reasoning text should appear with open circle icon and italic markup
- expect(result.markdown).toContain("ā");
+ expect(result.markdown).toContain("");
expect(result.markdown).toContain("Let me reason through this problem step by step.");
// Regular text should appear without open circle
expect(result.markdown).toContain("Here is my response.");
diff --git a/actions/setup/js/parse_codex_log.cjs b/actions/setup/js/parse_codex_log.cjs
index bf76aad5905..2416d7b56e7 100644
--- a/actions/setup/js/parse_codex_log.cjs
+++ b/actions/setup/js/parse_codex_log.cjs
@@ -1,7 +1,7 @@
// @ts-check
///
-const { createEngineLogParser, truncateString, estimateTokens, formatToolCallAsDetails, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
+const { createEngineLogParser, truncateString, estimateTokens, formatToolCallAsDetails, buildStepSummaryDetailsSection, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
const main = createEngineLogParser({
parserName: "Codex",
@@ -414,15 +414,15 @@ function parseCodexJsonl(logContent) {
// Build markdown so the parser returns a truthy result and core.info has a
// readable fallback. The step summary itself is rendered from logEntries.
- let markdown = "## š¤ Reasoning\n\n";
+ let markdown = "### Reasoning\n\n\nShow reasoning
\n\n";
for (const item of parsedData) {
if (item.type === "text") {
markdown += `${item.content}\n\n`;
} else if (item.type === "thinking") {
- markdown += `ā ${item.content}\n\n`;
+ markdown += `${item.content}\n\n`;
}
}
- markdown += "## š¤ Commands and Tools\n\n";
+ markdown += " \n\n### Commands and Tools\n\n\nShow commands and tool calls
\n\n";
for (const item of parsedData) {
if (item.type === "tool") {
const toolNameValue = item.toolName || "unknown-server__unknown-tool";
@@ -432,7 +432,7 @@ function parseCodexJsonl(logContent) {
markdown += formatCodexBashCall(item.content || "", item.response || "", item.statusIcon || DEFAULT_STATUS_ICON);
}
}
- markdown += "\n## š Information\n\n";
+ markdown += " \n\n### Information\n\n\nShow run metadata
\n\n";
if (usage) {
const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
@@ -441,6 +441,7 @@ function parseCodexJsonl(logContent) {
markdown += `**Total Tokens Used:** ${totalTokens.toLocaleString()}\n\n`;
}
}
+ markdown += " \n\n";
const logEntries = convertToLogEntries(parsedData);
@@ -490,7 +491,7 @@ function parseCodexLog(logContent) {
}
if (!logContent) {
return {
- markdown: "## š¤ Commands and Tools\n\nNo log content provided.\n\n## š¤ Reasoning\n\nUnable to parse reasoning from log.\n\n",
+ markdown: buildStepSummaryDetailsSection("Commands and Tools", "Show commands and tool calls", "No log content provided.") + buildStepSummaryDetailsSection("Reasoning", "Show reasoning", "Unable to parse reasoning from log."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
@@ -509,23 +510,23 @@ function parseCodexLog(logContent) {
// Extract MCP initialization information
const mcpInfo = extractMCPInitialization(lines);
if (mcpInfo.hasInfo) {
- markdown += "## š Initialization\n\n";
- markdown += mcpInfo.markdown;
+ markdown += buildStepSummaryDetailsSection("Initialization", "Show initialization details", mcpInfo.markdown);
}
// Extract error messages (e.g., model access blocked, cyber_policy_violation)
const errorInfo = extractCodexErrorMessages(lines);
if (errorInfo.hasErrors) {
- markdown += "## ā ļø Errors\n\n";
+ markdown += "### Errors\n\n\nShow errors
\n\n";
for (const message of errorInfo.messages) {
markdown += `> ${message}\n\n`;
}
if (errorInfo.reconnectCount > 0) {
markdown += `> Reconnect attempts: ${errorInfo.reconnectCount}/${errorInfo.maxReconnects}\n\n`;
}
+ markdown += " \n\n";
}
- markdown += "## š¤ Reasoning\n\n";
+ markdown += "### Reasoning\n\n\nShow reasoning
\n\n";
// Second pass: process full conversation flow with interleaved reasoning and tools
let inThinkingSection = false;
@@ -648,7 +649,7 @@ function parseCodexLog(logContent) {
const trimmed = line.trim();
thinkingContent.push(trimmed);
// Add thinking content directly to markdown with open circle icon and italic styling
- markdown += `ā ${trimmed}\n\n`;
+ markdown += `${trimmed}\n\n`;
}
}
@@ -660,7 +661,7 @@ function parseCodexLog(logContent) {
});
}
- markdown += "## š¤ Commands and Tools\n\n";
+ markdown += " \n\n### Commands and Tools\n\n\nShow commands and tool calls
\n\n";
// First pass: collect tool calls with details
for (let i = 0; i < lines.length; i++) {
@@ -787,7 +788,7 @@ function parseCodexLog(logContent) {
}
// Add Information section
- markdown += "\n## š Information\n\n";
+ markdown += " \n\n### Information\n\n\nShow run metadata
\n\n";
// Extract metadata from Codex logs
let totalTokens = 0;
@@ -816,6 +817,7 @@ function parseCodexLog(logContent) {
if (toolCalls > 0) {
markdown += `**Tool Calls:** ${toolCalls}\n\n`;
}
+ markdown += " \n\n";
// Convert parsed data to logEntries format
const logEntries = convertToLogEntries(parsedData);
diff --git a/actions/setup/js/parse_codex_log.test.cjs b/actions/setup/js/parse_codex_log.test.cjs
index 4451bc32a8d..274b0f3a600 100644
--- a/actions/setup/js/parse_codex_log.test.cjs
+++ b/actions/setup/js/parse_codex_log.test.cjs
@@ -50,8 +50,8 @@ github.list_pull_requests(...) success in 123ms:
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("## š¤ Reasoning");
- expect(result.markdown).toContain("## š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Reasoning");
+ expect(result.markdown).toContain("### Commands and Tools");
expect(result.markdown).toContain("github::list_pull_requests");
expect(result.markdown).toContain("ā
");
});
@@ -74,7 +74,7 @@ Let me start by listing the files in the root directory`;
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("## š¤ Reasoning");
+ expect(result.markdown).toContain("### Reasoning");
expect(result.markdown).toContain("I need to analyze the repository structure");
expect(result.markdown).toContain("Let me start by listing the files");
});
@@ -85,9 +85,7 @@ I need to analyze the repository structure to understand the codebase`;
const result = parseCodexLog(logContent);
- // Thinking content should be wrapped in italic markup with open circle icon
- expect(result.markdown).toContain("ā");
- expect(result.markdown).toContain("I need to analyze the repository structure");
+ expect(result.markdown).toContain("I need to analyze the repository structure");
});
it("should skip metadata lines", () => {
@@ -141,7 +139,7 @@ tokens used
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("š Information");
+ expect(result.markdown).toContain("### Information");
expect(result.markdown).toContain("Total Tokens Used");
expect(result.markdown).toContain("1,500");
});
@@ -159,8 +157,8 @@ ToolCall: github__add_labels {}`;
it("should handle empty log content", () => {
const result = parseCodexLog("");
- expect(result.markdown).toContain("## š¤ Reasoning");
- expect(result.markdown).toContain("## š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Reasoning");
+ expect(result.markdown).toContain("### Commands and Tools");
});
it("should handle log with errors gracefully", () => {
@@ -168,8 +166,8 @@ ToolCall: github__add_labels {}`;
const result = parseCodexLog(malformedLog);
expect(result.markdown).toContain("No log content provided");
- expect(result.markdown).toContain("## š¤ Commands and Tools");
- expect(result.markdown).toContain("## š¤ Reasoning");
+ expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("### Reasoning");
});
it("should handle tool calls without responses", () => {
@@ -199,7 +197,7 @@ x`;
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("š Information");
+ expect(result.markdown).toContain("### Information");
expect(result.markdown).toContain("**Tool Calls:** 1");
});
@@ -525,14 +523,14 @@ I will now use the GitHub API to list issues`;
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("## š Initialization");
+ expect(result.markdown).toContain("### Initialization");
expect(result.markdown).toContain("**MCP Servers:**");
expect(result.markdown).toContain("Total: 2");
expect(result.markdown).toContain("Connected: 2");
expect(result.markdown).toContain("ā
");
expect(result.markdown).toContain("github");
expect(result.markdown).toContain("safe_outputs");
- expect(result.markdown).toContain("## š¤ Reasoning");
+ expect(result.markdown).toContain("### Reasoning");
});
it("should skip initialization section when no MCP info present", () => {
@@ -542,8 +540,8 @@ I will analyze the code`;
const result = parseCodexLog(logContent);
- expect(result.markdown).not.toContain("## š Initialization");
- expect(result.markdown).toContain("## š¤ Reasoning");
+ expect(result.markdown).not.toContain("### Initialization");
+ expect(result.markdown).toContain("### Reasoning");
});
});
@@ -621,7 +619,7 @@ ERROR: stream disconnected before completion: This user's access to gpt-5.3-code
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("## ā ļø Errors");
+ expect(result.markdown).toContain("### Errors");
expect(result.markdown).toContain("stream disconnected before completion");
expect(result.markdown).toContain("cybersecurity");
});
@@ -634,7 +632,7 @@ ERROR: stream disconnected before completion: This user's access to gpt-5.3-code
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("## ā ļø Errors");
+ expect(result.markdown).toContain("### Errors");
expect(result.markdown).toContain("Reconnect attempts: 3/5");
});
@@ -647,7 +645,7 @@ github.list_pull_requests(...) success in 123ms:
const result = parseCodexLog(logContent);
- expect(result.markdown).not.toContain("## ā ļø Errors");
+ expect(result.markdown).not.toContain("### Errors");
});
it("should place Errors section before Reasoning section", () => {
@@ -655,8 +653,8 @@ github.list_pull_requests(...) success in 123ms:
const result = parseCodexLog(logContent);
- const errorsIndex = result.markdown.indexOf("## ā ļø Errors");
- const reasoningIndex = result.markdown.indexOf("## š¤ Reasoning");
+ const errorsIndex = result.markdown.indexOf("### Errors");
+ const reasoningIndex = result.markdown.indexOf("### Reasoning");
expect(errorsIndex).toBeGreaterThan(-1);
expect(reasoningIndex).toBeGreaterThan(-1);
expect(errorsIndex).toBeLessThan(reasoningIndex);
diff --git a/actions/setup/js/parse_copilot_log.cjs b/actions/setup/js/parse_copilot_log.cjs
index c94b994e0d5..ad28b08c17b 100644
--- a/actions/setup/js/parse_copilot_log.cjs
+++ b/actions/setup/js/parse_copilot_log.cjs
@@ -5,6 +5,7 @@ const {
createEngineLogParser,
generateConversationMarkdown,
generateInformationSection,
+ buildStepSummaryDetailsSection,
formatInitializationSummary,
formatToolUse,
parseLogEntries,
@@ -119,7 +120,7 @@ function parseCopilotLog(logContent) {
}
if (!logEntries || logEntries.length === 0) {
- return { markdown: "## Agent Log Summary\n\nLog format not recognized as Copilot JSON array or JSONL.\n", logEntries: [] };
+ return { markdown: buildStepSummaryDetailsSection("Agent Log Summary", "Show parser status", "Log format not recognized as Copilot JSON array or JSONL."), logEntries: [] };
}
const isEventFormat = isCopilotSdkEventsFormat(logEntries);
@@ -190,11 +191,11 @@ function parseCopilotLog(logContent) {
const awfTokenWarnings = extractAwfTokenWarnings(canonicalLogEntries);
if (awfTokenWarnings.length > 0) {
- markdown += "## ā ļø Firewall Steering\n\n";
+ let steeringBody = "";
for (const warning of awfTokenWarnings) {
- markdown += `- ${warning}\n`;
+ steeringBody += `- ${warning}\n`;
}
- markdown += "\n";
+ markdown += buildStepSummaryDetailsSection("Firewall Steering", "Show firewall steering notices", steeringBody);
}
// Add Information section
diff --git a/actions/setup/js/parse_copilot_log.test.cjs b/actions/setup/js/parse_copilot_log.test.cjs
index 28acbddbd8a..f0f32b4fc4b 100644
--- a/actions/setup/js/parse_copilot_log.test.cjs
+++ b/actions/setup/js/parse_copilot_log.test.cjs
@@ -74,8 +74,8 @@ describe("parse_copilot_log.cjs", () => {
]);
const result = parseCopilotLog(jsonArrayLog);
- expect(result.markdown).toContain("š Initialization");
- expect(result.markdown).toContain("š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("### Commands and Tools");
expect(result.markdown).toContain("copilot-test-123");
expect(result.markdown).toContain("echo 'Hello World'");
expect(result.markdown).toContain("Total Cost");
@@ -88,8 +88,8 @@ describe("parse_copilot_log.cjs", () => {
'[DEBUG] Starting Copilot CLI\n[ERROR] Some error occurred\n[{"type":"system","subtype":"init","session_id":"copilot-456","tools":["Bash","mcp__safe_outputs__missing-tool"],"model":"gpt-5"},{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool_123","name":"mcp__safe_outputs__missing-tool","input":{"tool":"draw_pelican","reason":"Tool needed to draw pelican artwork"}}]}},{"type":"result","total_cost_usd":0.1789264,"usage":{"input_tokens":25,"output_tokens":832},"num_turns":10}]\n[DEBUG] Session completed'
);
- expect(result.markdown).toContain("š Initialization");
- expect(result.markdown).toContain("š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("### Commands and Tools");
expect(result.markdown).toContain("copilot-456");
expect(result.markdown).toContain("safe_outputs::missing-tool");
expect(result.markdown).toContain("Total Cost");
@@ -100,8 +100,8 @@ describe("parse_copilot_log.cjs", () => {
'[DEBUG] Starting Copilot CLI\n{"type":"system","subtype":"init","session_id":"copilot-789","tools":["Bash","Read"],"model":"gpt-5"}\n[DEBUG] Processing user prompt\n{"type":"assistant","message":{"content":[{"type":"text","text":"I\'ll help you."},{"type":"tool_use","id":"tool_123","name":"Bash","input":{"command":"ls -la"}}]}}\n{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool_123","content":"file1.txt\\nfile2.txt"}]}}\n{"type":"result","total_cost_usd":0.002,"usage":{"input_tokens":100,"output_tokens":25},"num_turns":2}\n[DEBUG] Workflow completed'
);
- expect(result.markdown).toContain("š Initialization");
- expect(result.markdown).toContain("š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("### Commands and Tools");
expect(result.markdown).toContain("copilot-789");
expect(result.markdown).toContain("ls -la");
expect(result.markdown).toContain("Total Cost");
@@ -117,7 +117,7 @@ describe("parse_copilot_log.cjs", () => {
const result = parseCopilotLog(sdkEventsLog);
- expect(result.markdown).toContain("š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Commands and Tools");
expect(result.markdown).toContain("report_intent");
expect(result.markdown).toContain("Rendered summary content");
const resultData = getSessionResultData(result.logEntries);
@@ -222,7 +222,7 @@ describe("parse_copilot_log.cjs", () => {
const result = parseCopilotLog(prettyLog);
- expect(result.markdown).toContain("š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Commands and Tools");
// MCP tool name is formatted as server::name
expect(result.markdown).toContain("github::create_issue");
// Continuation output appears in the details
@@ -235,7 +235,7 @@ describe("parse_copilot_log.cjs", () => {
const result = parseCopilotLog(prettyLog);
- expect(result.markdown).toContain("š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Commands and Tools");
// Read tool name appears in the Reasoning section summary
expect(result.markdown).toContain("Read");
// Continuation output appears in the details
@@ -412,8 +412,7 @@ describe("parse_copilot_log.cjs", () => {
const result = parseCopilotLog(debugLog);
- // Reasoning should appear with open circle icon
- expect(result.markdown).toContain("ā");
+ // Reasoning should appear in the reasoning section
expect(result.markdown).toContain("I need to think carefully about the approach.");
// Regular content should appear without open circle
expect(result.markdown).toContain("Here is my answer.");
diff --git a/actions/setup/js/parse_custom_log.cjs b/actions/setup/js/parse_custom_log.cjs
index c903a8366ba..33b5dbdfe68 100644
--- a/actions/setup/js/parse_custom_log.cjs
+++ b/actions/setup/js/parse_custom_log.cjs
@@ -1,7 +1,7 @@
// @ts-check
///
-const { createEngineLogParser } = require("./log_parser_shared.cjs");
+const { createEngineLogParser, buildStepSummaryDetailsSection } = require("./log_parser_shared.cjs");
const main = createEngineLogParser({
parserName: "Custom",
@@ -25,7 +25,7 @@ function parseCustomLog(logContent) {
if (claudeResult && claudeResult.logEntries && claudeResult.logEntries.length > 0) {
return {
...claudeResult,
- markdown: "## Custom Engine Log (Claude format)\n\n" + claudeResult.markdown,
+ markdown: `### Custom Engine Log (Claude format)\n\n${claudeResult.markdown}`,
};
}
} catch (error) {
@@ -41,7 +41,7 @@ function parseCustomLog(logContent) {
// Check if we got meaningful content
if (codexResult && codexResult.markdown && codexResult.markdown.length > 0) {
return {
- markdown: "## Custom Engine Log (Codex format)\n\n" + codexResult.markdown,
+ markdown: `### Custom Engine Log (Codex format)\n\n${codexResult.markdown}`,
mcpFailures: codexResult.mcpFailures || [],
maxTurnsHit: codexResult.maxTurnsHit || false,
logEntries: codexResult.logEntries || [],
@@ -56,9 +56,10 @@ function parseCustomLog(logContent) {
const charCount = logContent.length;
return {
- markdown: `## Custom Engine Log
-
-Log format not recognized as Claude or Codex format.
+ markdown: buildStepSummaryDetailsSection(
+ "Custom Engine Log",
+ "Show parser status and raw log preview",
+ `Log format not recognized as Claude or Codex format.
**Basic Statistics:**
- Lines: ${lineCount}
@@ -67,8 +68,8 @@ Log format not recognized as Claude or Codex format.
**Raw Log Preview:**
\`\`\`
${logContent.substring(0, 1000)}${logContent.length > 1000 ? "\n... (truncated)" : ""}
-\`\`\`
-`,
+\`\`\``
+ ),
mcpFailures: [],
maxTurnsHit: false,
logEntries: [],
diff --git a/actions/setup/js/parse_gemini_log.cjs b/actions/setup/js/parse_gemini_log.cjs
index 9c3615eec7f..40e6a7d5459 100644
--- a/actions/setup/js/parse_gemini_log.cjs
+++ b/actions/setup/js/parse_gemini_log.cjs
@@ -1,7 +1,15 @@
// @ts-check
///
-const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
+const {
+ createEngineLogParser,
+ generateConversationMarkdown,
+ generateInformationSection,
+ buildStepSummaryDetailsSection,
+ formatInitializationSummary,
+ formatToolUse,
+ convertLegacyLogEntriesToCopilotEvents,
+} = require("./log_parser_shared.cjs");
const main = createEngineLogParser({
parserName: "Gemini",
@@ -23,7 +31,7 @@ const main = createEngineLogParser({
function parseGeminiLog(logContent) {
if (!logContent) {
return {
- markdown: "## š¤ Gemini\n\nNo log content provided.\n\n",
+ markdown: buildStepSummaryDetailsSection("Gemini", "Show parser status", "No log content provided."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
@@ -47,7 +55,7 @@ function parseGeminiLog(logContent) {
if (rawEntries.length === 0) {
return {
- markdown: "## š¤ Gemini\n\nLog format not recognized as Gemini JSONL.\n\n",
+ markdown: buildStepSummaryDetailsSection("Gemini", "Show parser status", "Log format not recognized as Gemini JSONL."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
diff --git a/actions/setup/js/parse_gemini_log.test.cjs b/actions/setup/js/parse_gemini_log.test.cjs
index 4197e4e54d6..65ddf72c2ab 100644
--- a/actions/setup/js/parse_gemini_log.test.cjs
+++ b/actions/setup/js/parse_gemini_log.test.cjs
@@ -55,7 +55,7 @@ describe("parse_gemini_log.cjs", () => {
const result = parseGeminiLog(logContent);
- expect(result.markdown).toContain("## š Initialization");
+ expect(result.markdown).toContain("### Initialization");
expect(result.markdown).toContain("gemini-2.0-flash");
expect(result.markdown).toContain("sess-123");
});
@@ -65,7 +65,7 @@ describe("parse_gemini_log.cjs", () => {
const result = parseGeminiLog(logContent);
- expect(result.markdown).toContain("## š¤ Reasoning");
+ expect(result.markdown).toContain("### Reasoning");
expect(result.markdown).toContain("I will analyze the repository.");
});
@@ -111,7 +111,7 @@ describe("parse_gemini_log.cjs", () => {
const result = parseGeminiLog(logContent);
- expect(result.markdown).toContain("## š Information");
+ expect(result.markdown).toContain("### Information");
expect(result.markdown).toContain("900");
expect(result.markdown).toContain("100");
});
@@ -129,13 +129,13 @@ describe("parse_gemini_log.cjs", () => {
const result = parseGeminiLog(logContent);
- expect(result.markdown).toContain("## š Initialization");
+ expect(result.markdown).toContain("### Initialization");
expect(result.markdown).toContain("auto-gemini-3");
- expect(result.markdown).toContain("## š¤ Reasoning");
+ expect(result.markdown).toContain("### Reasoning");
expect(result.markdown).toContain("I will list the PRs.");
- expect(result.markdown).toContain("## š¤ Commands and Tools");
+ expect(result.markdown).toContain("### Commands and Tools");
expect(result.markdown).toContain("list_pull_requests");
- expect(result.markdown).toContain("## š Information");
+ expect(result.markdown).toContain("### Information");
expect(result.logEntries.length).toBeGreaterThan(0);
expect(result.mcpFailures).toEqual([]);
expect(result.maxTurnsHit).toBe(false);
diff --git a/actions/setup/js/parse_pi_log.cjs b/actions/setup/js/parse_pi_log.cjs
index 0ac059be3be..1858b884c5f 100644
--- a/actions/setup/js/parse_pi_log.cjs
+++ b/actions/setup/js/parse_pi_log.cjs
@@ -1,7 +1,15 @@
// @ts-check
///
-const { createEngineLogParser, generateConversationMarkdown, generateInformationSection, formatInitializationSummary, formatToolUse, convertLegacyLogEntriesToCopilotEvents } = require("./log_parser_shared.cjs");
+const {
+ createEngineLogParser,
+ generateConversationMarkdown,
+ generateInformationSection,
+ buildStepSummaryDetailsSection,
+ formatInitializationSummary,
+ formatToolUse,
+ convertLegacyLogEntriesToCopilotEvents,
+} = require("./log_parser_shared.cjs");
const main = createEngineLogParser({
parserName: "Pi",
@@ -23,7 +31,7 @@ const main = createEngineLogParser({
function parsePiLog(logContent) {
if (!logContent) {
return {
- markdown: "## š¤ Pi\n\nNo log content provided.\n\n",
+ markdown: buildStepSummaryDetailsSection("Pi", "Show parser status", "No log content provided."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
@@ -46,7 +54,7 @@ function parsePiLog(logContent) {
if (rawEntries.length === 0) {
return {
- markdown: "## š¤ Pi\n\nLog format not recognized as Pi JSONL.\n\n",
+ markdown: buildStepSummaryDetailsSection("Pi", "Show parser status", "Log format not recognized as Pi JSONL."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
diff --git a/actions/setup/js/parse_pi_log.test.cjs b/actions/setup/js/parse_pi_log.test.cjs
index ffb52fa55c1..820e13f4bb4 100644
--- a/actions/setup/js/parse_pi_log.test.cjs
+++ b/actions/setup/js/parse_pi_log.test.cjs
@@ -55,7 +55,7 @@ describe("parse_pi_log.cjs", () => {
const result = parsePiLog(logContent);
- expect(result.markdown).toContain("## š Initialization");
+ expect(result.markdown).toContain("### Initialization");
expect(result.markdown).toContain("pi-3");
expect(result.markdown).toContain("sess-abc");
});
@@ -65,7 +65,7 @@ describe("parse_pi_log.cjs", () => {
const result = parsePiLog(logContent);
- expect(result.markdown).toContain("## š¤ Reasoning");
+ expect(result.markdown).toContain("### Reasoning");
expect(result.markdown).toContain("I will analyze the repository.");
});
From ca7bf38d48f34e0deb3e55c0c29fd63eaaf990c1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Jul 2026 07:19:34 +0000
Subject: [PATCH 2/8] Polish step summary section helpers
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/log_parser_format.cjs | 3 +--
actions/setup/js/log_parser_shared.cjs | 10 +++++-----
2 files changed, 6 insertions(+), 7 deletions(-)
diff --git a/actions/setup/js/log_parser_format.cjs b/actions/setup/js/log_parser_format.cjs
index 2665a63d89a..264ea3c7aeb 100644
--- a/actions/setup/js/log_parser_format.cjs
+++ b/actions/setup/js/log_parser_format.cjs
@@ -96,7 +96,7 @@ function createLogParserFormatters(deps) {
*/
function buildStepSummaryDetailsSection(title, summary, body) {
const trimmedBody = typeof body === "string" ? body.trim() : "";
- const content = trimmedBody || `No ${title.toLowerCase()} available.`;
+ const content = trimmedBody || "No details available.";
return `### ${title}\n\n\n${summary}
\n\n${content}\n \n\n`;
}
@@ -160,7 +160,6 @@ function createLogParserFormatters(deps) {
let text = content.thinking.trim();
text = unfenceMarkdown(text);
if (text) {
- commandDetailsBody += "";
reasoningBody += `${text.replace(/\n/g, "
")}\n\n`;
}
} else if (content.type === "tool_use") {
diff --git a/actions/setup/js/log_parser_shared.cjs b/actions/setup/js/log_parser_shared.cjs
index 249ade5c705..aca6ab2cc83 100644
--- a/actions/setup/js/log_parser_shared.cjs
+++ b/actions/setup/js/log_parser_shared.cjs
@@ -267,14 +267,14 @@ function isLikelyCustomAgent(toolName) {
* @param {string} title
* @param {string} summary
* @param {string} body
- * @param {{open?: boolean}} [options]
+ * @param {{open?: boolean, emptyBodyMessage?: string}} [options]
* @returns {string}
*/
function buildStepSummaryDetailsSection(title, summary, body, options = {}) {
- const { open = false } = options;
+ const { open = false, emptyBodyMessage = "No details available." } = options;
const openAttr = open ? " open" : "";
const trimmedBody = typeof body === "string" ? body.trim() : "";
- const content = trimmedBody || `No ${title.toLowerCase()} available.`;
+ const content = trimmedBody || emptyBodyMessage;
return `### ${title}\n\n\n${summary}
\n\n${content}\n \n\n`;
}
@@ -291,7 +291,7 @@ function generateInformationSection(lastEntry, options = {}) {
let markdown = "";
if (!lastEntry) {
- return buildStepSummaryDetailsSection("Information", "Show run metadata", "");
+ return buildStepSummaryDetailsSection("Information", "Show run metadata", "", { emptyBodyMessage: "No information available." });
}
if (lastEntry.num_turns) {
@@ -349,7 +349,7 @@ function generateInformationSection(lastEntry, options = {}) {
markdown += `**Permission Denials:** ${lastEntry.permission_denials.length}\n\n`;
}
- return buildStepSummaryDetailsSection("Information", "Show run metadata", markdown);
+ return buildStepSummaryDetailsSection("Information", "Show run metadata", markdown, { emptyBodyMessage: "No information available." });
}
/**
From 5c2cfe947380e71620af48d989649f3a822be20b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Jul 2026 08:01:15 +0000
Subject: [PATCH 3/8] Use details summaries as step headers
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/log_parser_format.cjs | 6 ++--
actions/setup/js/log_parser_shared.cjs | 6 ++--
actions/setup/js/log_parser_shared.test.cjs | 14 ++++----
actions/setup/js/parse_claude_log.test.cjs | 20 ++++++------
actions/setup/js/parse_codex_log.cjs | 14 ++++----
actions/setup/js/parse_codex_log.test.cjs | 36 ++++++++++-----------
actions/setup/js/parse_copilot_log.test.cjs | 18 +++++------
actions/setup/js/parse_gemini_log.test.cjs | 14 ++++----
actions/setup/js/parse_pi_log.test.cjs | 4 +--
9 files changed, 68 insertions(+), 64 deletions(-)
diff --git a/actions/setup/js/log_parser_format.cjs b/actions/setup/js/log_parser_format.cjs
index 264ea3c7aeb..ca40114f40f 100644
--- a/actions/setup/js/log_parser_format.cjs
+++ b/actions/setup/js/log_parser_format.cjs
@@ -88,7 +88,8 @@ function createLogParserFormatters(deps) {
}
/**
- * Builds a step-summary section with a plain-text h3 heading and collapsible details body.
+ * Builds a step-summary section with a collapsible details body whose summary
+ * acts as the section header.
* @param {string} title
* @param {string} summary
* @param {string} body
@@ -97,7 +98,8 @@ function createLogParserFormatters(deps) {
function buildStepSummaryDetailsSection(title, summary, body) {
const trimmedBody = typeof body === "string" ? body.trim() : "";
const content = trimmedBody || "No details available.";
- return `### ${title}\n\n\n${summary}
\n\n${content}\n \n\n`;
+ const summaryText = typeof title === "string" && title.trim() ? title : summary;
+ return `\n${summaryText}
\n\n${content}\n \n\n`;
}
/**
diff --git a/actions/setup/js/log_parser_shared.cjs b/actions/setup/js/log_parser_shared.cjs
index aca6ab2cc83..3dead3172cb 100644
--- a/actions/setup/js/log_parser_shared.cjs
+++ b/actions/setup/js/log_parser_shared.cjs
@@ -263,7 +263,8 @@ function isLikelyCustomAgent(toolName) {
}
/**
- * Builds a step-summary section with an h3 heading and collapsible details body.
+ * Builds a step-summary section with a collapsible details body whose summary
+ * acts as the section header.
* @param {string} title
* @param {string} summary
* @param {string} body
@@ -275,7 +276,8 @@ function buildStepSummaryDetailsSection(title, summary, body, options = {}) {
const openAttr = open ? " open" : "";
const trimmedBody = typeof body === "string" ? body.trim() : "";
const content = trimmedBody || emptyBodyMessage;
- return `### ${title}\n\n\n${summary}
\n\n${content}\n \n\n`;
+ const summaryText = typeof title === "string" && title.trim() ? title : summary;
+ return `\n${summaryText}
\n\n${content}\n \n\n`;
}
/**
diff --git a/actions/setup/js/log_parser_shared.test.cjs b/actions/setup/js/log_parser_shared.test.cjs
index 8699b32316e..9ec2a97e6ae 100644
--- a/actions/setup/js/log_parser_shared.test.cjs
+++ b/actions/setup/js/log_parser_shared.test.cjs
@@ -286,12 +286,12 @@ describe("log_parser_shared.cjs", () => {
formatInitCallback,
});
- expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("Initialization");
expect(result.markdown).toContain("Model: test-model");
- expect(result.markdown).toContain("### Reasoning");
+ expect(result.markdown).toContain("Reasoning");
expect(result.markdown).toContain("Let me help with that.");
expect(result.markdown).toContain("Tool: Bash");
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Commands and Tools");
expect(result.commandSummary).toHaveLength(1);
expect(result.commandSummary[0]).toContain("ā
");
expect(result.commandSummary[0]).toContain("echo hello");
@@ -305,8 +305,8 @@ describe("log_parser_shared.cjs", () => {
formatInitCallback: () => "",
});
- expect(result.markdown).toContain("### Reasoning");
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Reasoning");
+ expect(result.markdown).toContain("Commands and Tools");
expect(result.markdown).toContain("No commands or tools used.");
expect(result.commandSummary).toHaveLength(0);
});
@@ -375,7 +375,7 @@ describe("log_parser_shared.cjs", () => {
const result = generateInformationSection(lastEntry);
- expect(result).toContain("### Information");
+ expect(result).toContain("Information");
expect(result).toContain("**Turns:** 5");
expect(result).toContain("**Duration:** 2m 5s");
expect(result).toContain("**Total Cost:** $0.0123");
@@ -475,7 +475,7 @@ describe("log_parser_shared.cjs", () => {
const result = generateInformationSection(null);
- expect(result).toContain("### Information");
+ expect(result).toContain("Information");
expect(result).toContain("No information available.");
});
});
diff --git a/actions/setup/js/parse_claude_log.test.cjs b/actions/setup/js/parse_claude_log.test.cjs
index 21412bbc4bc..09baa6d21bb 100644
--- a/actions/setup/js/parse_claude_log.test.cjs
+++ b/actions/setup/js/parse_claude_log.test.cjs
@@ -86,8 +86,8 @@ describe("parse_claude_log.cjs", () => {
]);
const result = parseClaudeLog(jsonArrayLog);
- expect(result.markdown).toContain("### Initialization");
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Initialization");
+ expect(result.markdown).toContain("Commands and Tools");
expect(result.markdown).toContain("test-123");
expect(result.markdown).toContain("echo 'Hello World'");
expect(result.markdown).toContain("Total Cost");
@@ -99,8 +99,8 @@ describe("parse_claude_log.cjs", () => {
'[DEBUG] Starting Claude Code CLI\n[ERROR] Some error occurred\nnpm warn exec The following package was not found\n[{"type":"system","subtype":"init","session_id":"29d324d8-1a92-43c6-8740-babc2875a1d6","tools":["Task","Bash","mcp__safe_outputs__missing-tool"],"model":"claude-sonnet-4-20250514"},{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool_123","name":"mcp__safe_outputs__missing-tool","input":{"tool":"draw_pelican","reason":"Tool needed to draw pelican artwork"}}]}},{"type":"result","total_cost_usd":0.1789264,"usage":{"input_tokens":25,"output_tokens":832},"num_turns":10}]\n[DEBUG] Session completed'
);
- expect(result.markdown).toContain("### Initialization");
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Initialization");
+ expect(result.markdown).toContain("Commands and Tools");
expect(result.markdown).toContain("29d324d8-1a92-43c6-8740-babc2875a1d6");
expect(result.markdown).toContain("safe_outputs::missing-tool");
expect(result.markdown).toContain("Total Cost");
@@ -112,8 +112,8 @@ describe("parse_claude_log.cjs", () => {
'[DEBUG] Starting Claude Code CLI\n{"type":"system","subtype":"init","session_id":"test-456","tools":["Bash","Read"],"model":"claude-sonnet-4-20250514"}\n[DEBUG] Processing user prompt\n{"type":"assistant","message":{"content":[{"type":"text","text":"I\'ll help you."},{"type":"tool_use","id":"tool_123","name":"Bash","input":{"command":"ls -la"}}]}}\n{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool_123","content":"file1.txt\\nfile2.txt"}]}}\n{"type":"result","total_cost_usd":0.002,"usage":{"input_tokens":100,"output_tokens":25},"num_turns":2}\n[DEBUG] Workflow completed'
);
- expect(result.markdown).toContain("### Initialization");
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Initialization");
+ expect(result.markdown).toContain("Commands and Tools");
expect(result.markdown).toContain("test-456");
expect(result.markdown).toContain("ls -la");
expect(result.markdown).toContain("Total Cost");
@@ -136,7 +136,7 @@ describe("parse_claude_log.cjs", () => {
]);
const result = parseClaudeLog(logWithFailures);
- expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("Initialization");
expect(result.markdown).toContain("failed_server (failed)");
expect(result.mcpFailures).toEqual(["failed_server"]);
});
@@ -164,7 +164,7 @@ describe("parse_claude_log.cjs", () => {
]);
const result = parseClaudeLog(logWithDetailedErrors);
- expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("Initialization");
expect(result.markdown).toContain("failed_with_error (failed)");
expect(result.markdown).toContain("**Error:** Connection timeout after 30s");
expect(result.markdown).toContain("**Stderr:**");
@@ -264,7 +264,7 @@ describe("parse_claude_log.cjs", () => {
it("should skip debug lines that look like arrays but aren't JSON", () => {
const result = parseClaudeLog('[DEBUG] Starting\n[INFO] Processing\n[{"type":"system","subtype":"init","session_id":"test","tools":["Bash"],"model":"claude-sonnet-4-20250514"}]\n[DEBUG] Done');
- expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("Initialization");
});
it("should handle tool use with MCP tools", () => {
@@ -568,7 +568,7 @@ describe("parse_claude_log.cjs", () => {
expect(result.markdown).toContain("Grep");
expect(result.markdown).toContain("Bash");
- const toolsSection = result.markdown.split("### Reasoning")[0];
+ const toolsSection = result.markdown.split("Reasoning")[0];
expect(toolsSection).not.toMatch(/and \d+ more/);
});
diff --git a/actions/setup/js/parse_codex_log.cjs b/actions/setup/js/parse_codex_log.cjs
index 2416d7b56e7..4f8837025fe 100644
--- a/actions/setup/js/parse_codex_log.cjs
+++ b/actions/setup/js/parse_codex_log.cjs
@@ -414,7 +414,7 @@ function parseCodexJsonl(logContent) {
// Build markdown so the parser returns a truthy result and core.info has a
// readable fallback. The step summary itself is rendered from logEntries.
- let markdown = "### Reasoning\n\n\nShow reasoning
\n\n";
+ let markdown = "\nReasoning
\n\n";
for (const item of parsedData) {
if (item.type === "text") {
markdown += `${item.content}\n\n`;
@@ -422,7 +422,7 @@ function parseCodexJsonl(logContent) {
markdown += `${item.content}\n\n`;
}
}
- markdown += " \n\n### Commands and Tools\n\n\nShow commands and tool calls
\n\n";
+ markdown += " \n\n\nCommands and Tools
\n\n";
for (const item of parsedData) {
if (item.type === "tool") {
const toolNameValue = item.toolName || "unknown-server__unknown-tool";
@@ -432,7 +432,7 @@ function parseCodexJsonl(logContent) {
markdown += formatCodexBashCall(item.content || "", item.response || "", item.statusIcon || DEFAULT_STATUS_ICON);
}
}
- markdown += " \n\n### Information\n\n\nShow run metadata
\n\n";
+ markdown += " \n\n\nInformation
\n\n";
if (usage) {
const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
@@ -516,7 +516,7 @@ function parseCodexLog(logContent) {
// Extract error messages (e.g., model access blocked, cyber_policy_violation)
const errorInfo = extractCodexErrorMessages(lines);
if (errorInfo.hasErrors) {
- markdown += "### Errors\n\n\nShow errors
\n\n";
+ markdown += "\nErrors
\n\n";
for (const message of errorInfo.messages) {
markdown += `> ${message}\n\n`;
}
@@ -526,7 +526,7 @@ function parseCodexLog(logContent) {
markdown += " \n\n";
}
- markdown += "### Reasoning\n\n\nShow reasoning
\n\n";
+ markdown += "\nReasoning
\n\n";
// Second pass: process full conversation flow with interleaved reasoning and tools
let inThinkingSection = false;
@@ -661,7 +661,7 @@ function parseCodexLog(logContent) {
});
}
- markdown += " \n\n### Commands and Tools\n\n\nShow commands and tool calls
\n\n";
+ markdown += " \n\n\nCommands and Tools
\n\n";
// First pass: collect tool calls with details
for (let i = 0; i < lines.length; i++) {
@@ -788,7 +788,7 @@ function parseCodexLog(logContent) {
}
// Add Information section
- markdown += " \n\n### Information\n\n\nShow run metadata
\n\n";
+ markdown += " \n\n\nInformation
\n\n";
// Extract metadata from Codex logs
let totalTokens = 0;
diff --git a/actions/setup/js/parse_codex_log.test.cjs b/actions/setup/js/parse_codex_log.test.cjs
index 274b0f3a600..74b0f3c37ed 100644
--- a/actions/setup/js/parse_codex_log.test.cjs
+++ b/actions/setup/js/parse_codex_log.test.cjs
@@ -50,8 +50,8 @@ github.list_pull_requests(...) success in 123ms:
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("### Reasoning");
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Reasoning
");
+ expect(result.markdown).toContain("Commands and Tools
");
expect(result.markdown).toContain("github::list_pull_requests");
expect(result.markdown).toContain("ā
");
});
@@ -74,7 +74,7 @@ Let me start by listing the files in the root directory`;
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("### Reasoning");
+ expect(result.markdown).toContain("Reasoning
");
expect(result.markdown).toContain("I need to analyze the repository structure");
expect(result.markdown).toContain("Let me start by listing the files");
});
@@ -139,7 +139,7 @@ tokens used
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("### Information");
+ expect(result.markdown).toContain("Information
");
expect(result.markdown).toContain("Total Tokens Used");
expect(result.markdown).toContain("1,500");
});
@@ -157,8 +157,8 @@ ToolCall: github__add_labels {}`;
it("should handle empty log content", () => {
const result = parseCodexLog("");
- expect(result.markdown).toContain("### Reasoning");
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Reasoning
");
+ expect(result.markdown).toContain("Commands and Tools
");
});
it("should handle log with errors gracefully", () => {
@@ -166,8 +166,8 @@ ToolCall: github__add_labels {}`;
const result = parseCodexLog(malformedLog);
expect(result.markdown).toContain("No log content provided");
- expect(result.markdown).toContain("### Commands and Tools");
- expect(result.markdown).toContain("### Reasoning");
+ expect(result.markdown).toContain("Commands and Tools
");
+ expect(result.markdown).toContain("Reasoning
");
});
it("should handle tool calls without responses", () => {
@@ -197,7 +197,7 @@ x`;
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("### Information");
+ expect(result.markdown).toContain("Information
");
expect(result.markdown).toContain("**Tool Calls:** 1");
});
@@ -523,14 +523,14 @@ I will now use the GitHub API to list issues`;
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("Initialization
");
expect(result.markdown).toContain("**MCP Servers:**");
expect(result.markdown).toContain("Total: 2");
expect(result.markdown).toContain("Connected: 2");
expect(result.markdown).toContain("ā
");
expect(result.markdown).toContain("github");
expect(result.markdown).toContain("safe_outputs");
- expect(result.markdown).toContain("### Reasoning");
+ expect(result.markdown).toContain("Reasoning
");
});
it("should skip initialization section when no MCP info present", () => {
@@ -540,8 +540,8 @@ I will analyze the code`;
const result = parseCodexLog(logContent);
- expect(result.markdown).not.toContain("### Initialization");
- expect(result.markdown).toContain("### Reasoning");
+ expect(result.markdown).not.toContain("Initialization
");
+ expect(result.markdown).toContain("Reasoning
");
});
});
@@ -619,7 +619,7 @@ ERROR: stream disconnected before completion: This user's access to gpt-5.3-code
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("### Errors");
+ expect(result.markdown).toContain("Errors
");
expect(result.markdown).toContain("stream disconnected before completion");
expect(result.markdown).toContain("cybersecurity");
});
@@ -632,7 +632,7 @@ ERROR: stream disconnected before completion: This user's access to gpt-5.3-code
const result = parseCodexLog(logContent);
- expect(result.markdown).toContain("### Errors");
+ expect(result.markdown).toContain("Errors
");
expect(result.markdown).toContain("Reconnect attempts: 3/5");
});
@@ -645,7 +645,7 @@ github.list_pull_requests(...) success in 123ms:
const result = parseCodexLog(logContent);
- expect(result.markdown).not.toContain("### Errors");
+ expect(result.markdown).not.toContain("Errors
");
});
it("should place Errors section before Reasoning section", () => {
@@ -653,8 +653,8 @@ github.list_pull_requests(...) success in 123ms:
const result = parseCodexLog(logContent);
- const errorsIndex = result.markdown.indexOf("### Errors");
- const reasoningIndex = result.markdown.indexOf("### Reasoning");
+ const errorsIndex = result.markdown.indexOf("Errors
");
+ const reasoningIndex = result.markdown.indexOf("Reasoning
");
expect(errorsIndex).toBeGreaterThan(-1);
expect(reasoningIndex).toBeGreaterThan(-1);
expect(errorsIndex).toBeLessThan(reasoningIndex);
diff --git a/actions/setup/js/parse_copilot_log.test.cjs b/actions/setup/js/parse_copilot_log.test.cjs
index f0f32b4fc4b..89f5c991d1b 100644
--- a/actions/setup/js/parse_copilot_log.test.cjs
+++ b/actions/setup/js/parse_copilot_log.test.cjs
@@ -74,8 +74,8 @@ describe("parse_copilot_log.cjs", () => {
]);
const result = parseCopilotLog(jsonArrayLog);
- expect(result.markdown).toContain("### Initialization");
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Initialization
");
+ expect(result.markdown).toContain("Commands and Tools
");
expect(result.markdown).toContain("copilot-test-123");
expect(result.markdown).toContain("echo 'Hello World'");
expect(result.markdown).toContain("Total Cost");
@@ -88,8 +88,8 @@ describe("parse_copilot_log.cjs", () => {
'[DEBUG] Starting Copilot CLI\n[ERROR] Some error occurred\n[{"type":"system","subtype":"init","session_id":"copilot-456","tools":["Bash","mcp__safe_outputs__missing-tool"],"model":"gpt-5"},{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool_123","name":"mcp__safe_outputs__missing-tool","input":{"tool":"draw_pelican","reason":"Tool needed to draw pelican artwork"}}]}},{"type":"result","total_cost_usd":0.1789264,"usage":{"input_tokens":25,"output_tokens":832},"num_turns":10}]\n[DEBUG] Session completed'
);
- expect(result.markdown).toContain("### Initialization");
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Initialization
");
+ expect(result.markdown).toContain("Commands and Tools
");
expect(result.markdown).toContain("copilot-456");
expect(result.markdown).toContain("safe_outputs::missing-tool");
expect(result.markdown).toContain("Total Cost");
@@ -100,8 +100,8 @@ describe("parse_copilot_log.cjs", () => {
'[DEBUG] Starting Copilot CLI\n{"type":"system","subtype":"init","session_id":"copilot-789","tools":["Bash","Read"],"model":"gpt-5"}\n[DEBUG] Processing user prompt\n{"type":"assistant","message":{"content":[{"type":"text","text":"I\'ll help you."},{"type":"tool_use","id":"tool_123","name":"Bash","input":{"command":"ls -la"}}]}}\n{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool_123","content":"file1.txt\\nfile2.txt"}]}}\n{"type":"result","total_cost_usd":0.002,"usage":{"input_tokens":100,"output_tokens":25},"num_turns":2}\n[DEBUG] Workflow completed'
);
- expect(result.markdown).toContain("### Initialization");
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Initialization
");
+ expect(result.markdown).toContain("Commands and Tools
");
expect(result.markdown).toContain("copilot-789");
expect(result.markdown).toContain("ls -la");
expect(result.markdown).toContain("Total Cost");
@@ -117,7 +117,7 @@ describe("parse_copilot_log.cjs", () => {
const result = parseCopilotLog(sdkEventsLog);
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Commands and Tools
");
expect(result.markdown).toContain("report_intent");
expect(result.markdown).toContain("Rendered summary content");
const resultData = getSessionResultData(result.logEntries);
@@ -222,7 +222,7 @@ describe("parse_copilot_log.cjs", () => {
const result = parseCopilotLog(prettyLog);
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Commands and Tools
");
// MCP tool name is formatted as server::name
expect(result.markdown).toContain("github::create_issue");
// Continuation output appears in the details
@@ -235,7 +235,7 @@ describe("parse_copilot_log.cjs", () => {
const result = parseCopilotLog(prettyLog);
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Commands and Tools
");
// Read tool name appears in the Reasoning section summary
expect(result.markdown).toContain("Read");
// Continuation output appears in the details
diff --git a/actions/setup/js/parse_gemini_log.test.cjs b/actions/setup/js/parse_gemini_log.test.cjs
index 65ddf72c2ab..ce9c63593d2 100644
--- a/actions/setup/js/parse_gemini_log.test.cjs
+++ b/actions/setup/js/parse_gemini_log.test.cjs
@@ -55,7 +55,7 @@ describe("parse_gemini_log.cjs", () => {
const result = parseGeminiLog(logContent);
- expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("Initialization
");
expect(result.markdown).toContain("gemini-2.0-flash");
expect(result.markdown).toContain("sess-123");
});
@@ -65,7 +65,7 @@ describe("parse_gemini_log.cjs", () => {
const result = parseGeminiLog(logContent);
- expect(result.markdown).toContain("### Reasoning");
+ expect(result.markdown).toContain("Reasoning
");
expect(result.markdown).toContain("I will analyze the repository.");
});
@@ -111,7 +111,7 @@ describe("parse_gemini_log.cjs", () => {
const result = parseGeminiLog(logContent);
- expect(result.markdown).toContain("### Information");
+ expect(result.markdown).toContain("Information
");
expect(result.markdown).toContain("900");
expect(result.markdown).toContain("100");
});
@@ -129,13 +129,13 @@ describe("parse_gemini_log.cjs", () => {
const result = parseGeminiLog(logContent);
- expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("Initialization
");
expect(result.markdown).toContain("auto-gemini-3");
- expect(result.markdown).toContain("### Reasoning");
+ expect(result.markdown).toContain("Reasoning
");
expect(result.markdown).toContain("I will list the PRs.");
- expect(result.markdown).toContain("### Commands and Tools");
+ expect(result.markdown).toContain("Commands and Tools
");
expect(result.markdown).toContain("list_pull_requests");
- expect(result.markdown).toContain("### Information");
+ expect(result.markdown).toContain("Information
");
expect(result.logEntries.length).toBeGreaterThan(0);
expect(result.mcpFailures).toEqual([]);
expect(result.maxTurnsHit).toBe(false);
diff --git a/actions/setup/js/parse_pi_log.test.cjs b/actions/setup/js/parse_pi_log.test.cjs
index 820e13f4bb4..6108362b573 100644
--- a/actions/setup/js/parse_pi_log.test.cjs
+++ b/actions/setup/js/parse_pi_log.test.cjs
@@ -55,7 +55,7 @@ describe("parse_pi_log.cjs", () => {
const result = parsePiLog(logContent);
- expect(result.markdown).toContain("### Initialization");
+ expect(result.markdown).toContain("Initialization
");
expect(result.markdown).toContain("pi-3");
expect(result.markdown).toContain("sess-abc");
});
@@ -65,7 +65,7 @@ describe("parse_pi_log.cjs", () => {
const result = parsePiLog(logContent);
- expect(result.markdown).toContain("### Reasoning");
+ expect(result.markdown).toContain("Reasoning
");
expect(result.markdown).toContain("I will analyze the repository.");
});
From 8b966251c4a2cddb2f99677711d5d2fb119592a8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Jul 2026 12:39:47 +0000
Subject: [PATCH 4/8] Remove misleading summary param from
buildStepSummaryDetailsSection
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
actions/setup/js/log_parser_format.cjs | 12 +++++-------
actions/setup/js/log_parser_shared.cjs | 10 ++++------
actions/setup/js/parse_antigravity_log.cjs | 6 +++---
actions/setup/js/parse_antigravity_log.test.cjs | 4 ++--
actions/setup/js/parse_claude_log.cjs | 2 +-
actions/setup/js/parse_codex_log.cjs | 4 ++--
actions/setup/js/parse_copilot_log.cjs | 4 ++--
actions/setup/js/parse_custom_log.cjs | 1 -
actions/setup/js/parse_gemini_log.cjs | 4 ++--
actions/setup/js/parse_pi_log.cjs | 4 ++--
10 files changed, 23 insertions(+), 28 deletions(-)
diff --git a/actions/setup/js/log_parser_format.cjs b/actions/setup/js/log_parser_format.cjs
index ca40114f40f..fd60228a976 100644
--- a/actions/setup/js/log_parser_format.cjs
+++ b/actions/setup/js/log_parser_format.cjs
@@ -91,15 +91,13 @@ function createLogParserFormatters(deps) {
* Builds a step-summary section with a collapsible details body whose summary
* acts as the section header.
* @param {string} title
- * @param {string} summary
* @param {string} body
* @returns {string}
*/
- function buildStepSummaryDetailsSection(title, summary, body) {
+ function buildStepSummaryDetailsSection(title, body) {
const trimmedBody = typeof body === "string" ? body.trim() : "";
const content = trimmedBody || "No details available.";
- const summaryText = typeof title === "string" && title.trim() ? title : summary;
- return `\n${summaryText}
\n\n${content}\n \n\n`;
+ return `\n${title}
\n\n${content}\n \n\n`;
}
/**
@@ -137,7 +135,7 @@ function createLogParserFormatters(deps) {
if (initEntry && formatInitCallback) {
const initResult = formatInitCallback(initEntry);
const initBody = typeof initResult === "string" ? initResult : initResult && initResult.markdown ? initResult.markdown : "";
- if (!addContent(buildStepSummaryDetailsSection("Initialization", "Show initialization details", initBody))) {
+ if (!addContent(buildStepSummaryDetailsSection("Initialization", initBody))) {
markdown += SIZE_LIMIT_WARNING;
return { markdown, commandSummary: [], sizeLimitReached };
}
@@ -174,7 +172,7 @@ function createLogParserFormatters(deps) {
}
}
- if (!addContent(buildStepSummaryDetailsSection("Reasoning", "Show reasoning", reasoningBody))) {
+ if (!addContent(buildStepSummaryDetailsSection("Reasoning", reasoningBody))) {
markdown += SIZE_LIMIT_WARNING;
return { markdown, commandSummary: [], sizeLimitReached };
}
@@ -229,7 +227,7 @@ function createLogParserFormatters(deps) {
commandsBody += commandDetailsBody.trim() + "\n";
}
- if (!addContent(buildStepSummaryDetailsSection("Commands and Tools", "Show commands and tool calls", commandsBody))) {
+ if (!addContent(buildStepSummaryDetailsSection("Commands and Tools", commandsBody))) {
markdown += SIZE_LIMIT_WARNING;
return { markdown, commandSummary, sizeLimitReached: true };
}
diff --git a/actions/setup/js/log_parser_shared.cjs b/actions/setup/js/log_parser_shared.cjs
index 3dead3172cb..8ae09048771 100644
--- a/actions/setup/js/log_parser_shared.cjs
+++ b/actions/setup/js/log_parser_shared.cjs
@@ -266,18 +266,16 @@ function isLikelyCustomAgent(toolName) {
* Builds a step-summary section with a collapsible details body whose summary
* acts as the section header.
* @param {string} title
- * @param {string} summary
* @param {string} body
* @param {{open?: boolean, emptyBodyMessage?: string}} [options]
* @returns {string}
*/
-function buildStepSummaryDetailsSection(title, summary, body, options = {}) {
+function buildStepSummaryDetailsSection(title, body, options = {}) {
const { open = false, emptyBodyMessage = "No details available." } = options;
const openAttr = open ? " open" : "";
const trimmedBody = typeof body === "string" ? body.trim() : "";
const content = trimmedBody || emptyBodyMessage;
- const summaryText = typeof title === "string" && title.trim() ? title : summary;
- return `\n${summaryText}
\n\n${content}\n \n\n`;
+ return `\n${title}
\n\n${content}\n \n\n`;
}
/**
@@ -293,7 +291,7 @@ function generateInformationSection(lastEntry, options = {}) {
let markdown = "";
if (!lastEntry) {
- return buildStepSummaryDetailsSection("Information", "Show run metadata", "", { emptyBodyMessage: "No information available." });
+ return buildStepSummaryDetailsSection("Information", "", { emptyBodyMessage: "No information available." });
}
if (lastEntry.num_turns) {
@@ -351,7 +349,7 @@ function generateInformationSection(lastEntry, options = {}) {
markdown += `**Permission Denials:** ${lastEntry.permission_denials.length}\n\n`;
}
- return buildStepSummaryDetailsSection("Information", "Show run metadata", markdown, { emptyBodyMessage: "No information available." });
+ return buildStepSummaryDetailsSection("Information", markdown, { emptyBodyMessage: "No information available." });
}
/**
diff --git a/actions/setup/js/parse_antigravity_log.cjs b/actions/setup/js/parse_antigravity_log.cjs
index 6e380472f70..a94ee4617ca 100644
--- a/actions/setup/js/parse_antigravity_log.cjs
+++ b/actions/setup/js/parse_antigravity_log.cjs
@@ -27,7 +27,7 @@ const main = createEngineLogParser({
function parseAntigravityLog(logContent) {
if (!logContent) {
return {
- markdown: buildStepSummaryDetailsSection("Antigravity", "Show parser status", "No log content provided."),
+ markdown: buildStepSummaryDetailsSection("Antigravity", "No log content provided."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
@@ -53,7 +53,7 @@ function parseAntigravityLog(logContent) {
if (parsedLines.length === 0) {
return {
- markdown: buildStepSummaryDetailsSection("Antigravity", "Show parser status", "Log format not recognized as Antigravity stream-json."),
+ markdown: buildStepSummaryDetailsSection("Antigravity", "Log format not recognized as Antigravity stream-json."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
@@ -66,7 +66,7 @@ function parseAntigravityLog(logContent) {
const stats = lastEntry.stats || {};
// Build markdown output
- let markdown = buildStepSummaryDetailsSection("Antigravity", "Show final response", finalResponse.trim());
+ let markdown = buildStepSummaryDetailsSection("Antigravity", finalResponse.trim());
// Compute aggregated token usage from all models
let totalInputTokens = 0;
diff --git a/actions/setup/js/parse_antigravity_log.test.cjs b/actions/setup/js/parse_antigravity_log.test.cjs
index ae2b7b10571..817c3838ced 100644
--- a/actions/setup/js/parse_antigravity_log.test.cjs
+++ b/actions/setup/js/parse_antigravity_log.test.cjs
@@ -107,7 +107,7 @@ describe("parse_antigravity_log.cjs", () => {
const result = parseAntigravityLog(logContent);
- expect(result.markdown).toContain("### Antigravity");
+ expect(result.markdown).toContain("Antigravity
");
expect(result.markdown).toContain("Hello from Antigravity");
expect(result.markdown).toContain("500");
expect(result.markdown).toContain("200");
@@ -128,7 +128,7 @@ describe("parse_antigravity_log.cjs", () => {
const result = parseAntigravityLog(logContent);
- expect(result.markdown).toContain("### Antigravity");
+ expect(result.markdown).toContain("Antigravity
");
expect(result.logEntries).toHaveLength(0);
});
diff --git a/actions/setup/js/parse_claude_log.cjs b/actions/setup/js/parse_claude_log.cjs
index fc3b783783a..477b6013d23 100644
--- a/actions/setup/js/parse_claude_log.cjs
+++ b/actions/setup/js/parse_claude_log.cjs
@@ -29,7 +29,7 @@ function parseClaudeLog(logContent) {
if (!logEntries) {
return {
- markdown: buildStepSummaryDetailsSection("Agent Log Summary", "Show parser status", "Log format not recognized as Claude JSON array or JSONL."),
+ markdown: buildStepSummaryDetailsSection("Agent Log Summary", "Log format not recognized as Claude JSON array or JSONL."),
mcpFailures: [],
maxTurnsHit: false,
logEntries: [],
diff --git a/actions/setup/js/parse_codex_log.cjs b/actions/setup/js/parse_codex_log.cjs
index 4f8837025fe..47087cbd227 100644
--- a/actions/setup/js/parse_codex_log.cjs
+++ b/actions/setup/js/parse_codex_log.cjs
@@ -491,7 +491,7 @@ function parseCodexLog(logContent) {
}
if (!logContent) {
return {
- markdown: buildStepSummaryDetailsSection("Commands and Tools", "Show commands and tool calls", "No log content provided.") + buildStepSummaryDetailsSection("Reasoning", "Show reasoning", "Unable to parse reasoning from log."),
+ markdown: buildStepSummaryDetailsSection("Commands and Tools", "No log content provided.") + buildStepSummaryDetailsSection("Reasoning", "Unable to parse reasoning from log."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
@@ -510,7 +510,7 @@ function parseCodexLog(logContent) {
// Extract MCP initialization information
const mcpInfo = extractMCPInitialization(lines);
if (mcpInfo.hasInfo) {
- markdown += buildStepSummaryDetailsSection("Initialization", "Show initialization details", mcpInfo.markdown);
+ markdown += buildStepSummaryDetailsSection("Initialization", mcpInfo.markdown);
}
// Extract error messages (e.g., model access blocked, cyber_policy_violation)
diff --git a/actions/setup/js/parse_copilot_log.cjs b/actions/setup/js/parse_copilot_log.cjs
index ad28b08c17b..b6641f3de45 100644
--- a/actions/setup/js/parse_copilot_log.cjs
+++ b/actions/setup/js/parse_copilot_log.cjs
@@ -120,7 +120,7 @@ function parseCopilotLog(logContent) {
}
if (!logEntries || logEntries.length === 0) {
- return { markdown: buildStepSummaryDetailsSection("Agent Log Summary", "Show parser status", "Log format not recognized as Copilot JSON array or JSONL."), logEntries: [] };
+ return { markdown: buildStepSummaryDetailsSection("Agent Log Summary", "Log format not recognized as Copilot JSON array or JSONL."), logEntries: [] };
}
const isEventFormat = isCopilotSdkEventsFormat(logEntries);
@@ -195,7 +195,7 @@ function parseCopilotLog(logContent) {
for (const warning of awfTokenWarnings) {
steeringBody += `- ${warning}\n`;
}
- markdown += buildStepSummaryDetailsSection("Firewall Steering", "Show firewall steering notices", steeringBody);
+ markdown += buildStepSummaryDetailsSection("Firewall Steering", steeringBody);
}
// Add Information section
diff --git a/actions/setup/js/parse_custom_log.cjs b/actions/setup/js/parse_custom_log.cjs
index 33b5dbdfe68..c8920633b76 100644
--- a/actions/setup/js/parse_custom_log.cjs
+++ b/actions/setup/js/parse_custom_log.cjs
@@ -58,7 +58,6 @@ function parseCustomLog(logContent) {
return {
markdown: buildStepSummaryDetailsSection(
"Custom Engine Log",
- "Show parser status and raw log preview",
`Log format not recognized as Claude or Codex format.
**Basic Statistics:**
diff --git a/actions/setup/js/parse_gemini_log.cjs b/actions/setup/js/parse_gemini_log.cjs
index 40e6a7d5459..c669bd7764a 100644
--- a/actions/setup/js/parse_gemini_log.cjs
+++ b/actions/setup/js/parse_gemini_log.cjs
@@ -31,7 +31,7 @@ const main = createEngineLogParser({
function parseGeminiLog(logContent) {
if (!logContent) {
return {
- markdown: buildStepSummaryDetailsSection("Gemini", "Show parser status", "No log content provided."),
+ markdown: buildStepSummaryDetailsSection("Gemini", "No log content provided."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
@@ -55,7 +55,7 @@ function parseGeminiLog(logContent) {
if (rawEntries.length === 0) {
return {
- markdown: buildStepSummaryDetailsSection("Gemini", "Show parser status", "Log format not recognized as Gemini JSONL."),
+ markdown: buildStepSummaryDetailsSection("Gemini", "Log format not recognized as Gemini JSONL."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
diff --git a/actions/setup/js/parse_pi_log.cjs b/actions/setup/js/parse_pi_log.cjs
index 1858b884c5f..d0edb48fd6f 100644
--- a/actions/setup/js/parse_pi_log.cjs
+++ b/actions/setup/js/parse_pi_log.cjs
@@ -31,7 +31,7 @@ const main = createEngineLogParser({
function parsePiLog(logContent) {
if (!logContent) {
return {
- markdown: buildStepSummaryDetailsSection("Pi", "Show parser status", "No log content provided."),
+ markdown: buildStepSummaryDetailsSection("Pi", "No log content provided."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
@@ -54,7 +54,7 @@ function parsePiLog(logContent) {
if (rawEntries.length === 0) {
return {
- markdown: buildStepSummaryDetailsSection("Pi", "Show parser status", "Log format not recognized as Pi JSONL."),
+ markdown: buildStepSummaryDetailsSection("Pi", "Log format not recognized as Pi JSONL."),
logEntries: [],
mcpFailures: [],
maxTurnsHit: false,
From e3fa97ca20a580c8c8af8125b2d676e4e53445be Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Jul 2026 18:27:40 +0000
Subject: [PATCH 5/8] chore: planning fixes for review feedback
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
.github/skills/agentic-workflows/SKILL.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md
index a7cbb15f3af..dc68e06a275 100644
--- a/.github/skills/agentic-workflows/SKILL.md
+++ b/.github/skills/agentic-workflows/SKILL.md
@@ -23,6 +23,7 @@ Load these files from `github/gh-aw` (they are not available locally).
- `.github/aw/charts.md`
- `.github/aw/cli-commands.md`
- `.github/aw/context.md`
+- `.github/aw/create-agentic-workflow-trigger-details.md`
- `.github/aw/create-agentic-workflow.md`
- `.github/aw/create-shared-agentic-workflow.md`
- `.github/aw/debug-agentic-workflow.md`
From 2258dad6fbb8db419e5e8e44467de085c62d17c4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Jul 2026 20:41:14 +0000
Subject: [PATCH 6/8] =?UTF-8?q?fix:=20address=20review=20feedback=20?=
=?UTF-8?q?=E2=80=94=20budget=20truncation=20and=20buildStepSummaryDetails?=
=?UTF-8?q?Section=20parity?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
.github/skills/agentic-workflows/SKILL.md | 1 -
actions/setup/js/log_parser_format.cjs | 70 +++++++++++++++++++----
actions/setup/js/log_parser_shared.cjs | 8 +++
3 files changed, 67 insertions(+), 12 deletions(-)
diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md
index dc68e06a275..a7cbb15f3af 100644
--- a/.github/skills/agentic-workflows/SKILL.md
+++ b/.github/skills/agentic-workflows/SKILL.md
@@ -23,7 +23,6 @@ Load these files from `github/gh-aw` (they are not available locally).
- `.github/aw/charts.md`
- `.github/aw/cli-commands.md`
- `.github/aw/context.md`
-- `.github/aw/create-agentic-workflow-trigger-details.md`
- `.github/aw/create-agentic-workflow.md`
- `.github/aw/create-shared-agentic-workflow.md`
- `.github/aw/debug-agentic-workflow.md`
diff --git a/actions/setup/js/log_parser_format.cjs b/actions/setup/js/log_parser_format.cjs
index fd60228a976..2f38ba8e10d 100644
--- a/actions/setup/js/log_parser_format.cjs
+++ b/actions/setup/js/log_parser_format.cjs
@@ -90,14 +90,22 @@ function createLogParserFormatters(deps) {
/**
* Builds a step-summary section with a collapsible details body whose summary
* acts as the section header.
+ *
+ * NOTE: This is a local copy of log_parser_shared.cjs:buildStepSummaryDetailsSection.
+ * It cannot be imported from there because log_parser_shared.cjs requires
+ * log_parser_format.cjs (circular dependency). Keep both copies in sync.
+ *
* @param {string} title
* @param {string} body
+ * @param {{open?: boolean, emptyBodyMessage?: string}} [options]
* @returns {string}
*/
- function buildStepSummaryDetailsSection(title, body) {
+ function buildStepSummaryDetailsSection(title, body, options = {}) {
+ const { open = false, emptyBodyMessage = "No details available." } = options;
+ const openAttr = open ? " open" : "";
const trimmedBody = typeof body === "string" ? body.trim() : "";
- const content = trimmedBody || "No details available.";
- return `\n${title}
\n\n${content}\n \n\n`;
+ const content = trimmedBody || emptyBodyMessage;
+ return `\n${title}
\n\n${content}\n \n\n`;
}
/**
@@ -131,6 +139,45 @@ function createLogParserFormatters(deps) {
return true;
}
+ /**
+ * Adds a details section, truncating the body when it would exceed the
+ * remaining step-summary budget. Emits partial content with a truncation
+ * note rather than dropping the entire section.
+ * @param {string} title
+ * @param {string} body
+ * @returns {boolean} True if any content was emitted
+ */
+ function addDetailsSectionFitting(title, body) {
+ const fullSection = buildStepSummaryDetailsSection(title, body);
+ if (addContent(fullSection)) {
+ return true;
+ }
+
+ // Full section doesn't fit ā try truncating the body to use what remains.
+ if (!summaryTracker) {
+ return false;
+ }
+
+ const truncationNote = "\n\n*(content truncated ā step summary size limit reached)*\n";
+ const truncNoteSize = Buffer.byteLength(truncationNote, "utf8");
+ const shell = `\n${title}
\n\n\n \n\n`;
+ const shellSize = Buffer.byteLength(shell, "utf8");
+ const availableForBody = summaryTracker.remaining() - shellSize - truncNoteSize;
+
+ if (availableForBody <= 0) {
+ return false;
+ }
+
+ // Truncate body at a clean UTF-8 character boundary.
+ const bodyBuf = Buffer.from(body, "utf8");
+ let cutoff = Math.min(availableForBody, bodyBuf.length);
+ while (cutoff > 0 && (bodyBuf[cutoff] & 0xc0) === 0x80) {
+ cutoff--;
+ }
+ const truncatedBody = bodyBuf.slice(0, cutoff).toString("utf8") + truncationNote;
+ return addContent(buildStepSummaryDetailsSection(title, truncatedBody));
+ }
+
const initEntry = renderEntries.find(entry => entry.type === "system" && entry.subtype === "init");
if (initEntry && formatInitCallback) {
const initResult = formatInitCallback(initEntry);
@@ -148,6 +195,9 @@ function createLogParserFormatters(deps) {
if (entry.type !== "assistant" || !entry.message?.content) {
continue;
}
+ if (summaryTracker && summaryTracker.isLimitReached()) {
+ break;
+ }
for (const content of entry.message.content) {
if (content.type === "text" && content.text) {
@@ -172,14 +222,9 @@ function createLogParserFormatters(deps) {
}
}
- if (!addContent(buildStepSummaryDetailsSection("Reasoning", reasoningBody))) {
+ if (!addDetailsSectionFitting("Reasoning", reasoningBody)) {
markdown += SIZE_LIMIT_WARNING;
- return { markdown, commandSummary: [], sizeLimitReached };
- }
-
- if (sizeLimitReached) {
- markdown += SIZE_LIMIT_WARNING;
- return { markdown, commandSummary: [], sizeLimitReached };
+ return { markdown, commandSummary: [], sizeLimitReached: true };
}
const commandSummary = [];
@@ -187,6 +232,9 @@ function createLogParserFormatters(deps) {
if (entry.type !== "assistant" || !entry.message?.content) {
continue;
}
+ if (summaryTracker && summaryTracker.isLimitReached()) {
+ break;
+ }
for (const content of entry.message.content) {
if (content.type !== "tool_use") {
@@ -227,7 +275,7 @@ function createLogParserFormatters(deps) {
commandsBody += commandDetailsBody.trim() + "\n";
}
- if (!addContent(buildStepSummaryDetailsSection("Commands and Tools", commandsBody))) {
+ if (!addDetailsSectionFitting("Commands and Tools", commandsBody)) {
markdown += SIZE_LIMIT_WARNING;
return { markdown, commandSummary, sizeLimitReached: true };
}
diff --git a/actions/setup/js/log_parser_shared.cjs b/actions/setup/js/log_parser_shared.cjs
index 8ae09048771..618c355ae32 100644
--- a/actions/setup/js/log_parser_shared.cjs
+++ b/actions/setup/js/log_parser_shared.cjs
@@ -118,6 +118,14 @@ class StepSummaryTracker {
return this.limitReached;
}
+ /**
+ * Gets the remaining byte capacity before the limit.
+ * @returns {number} Remaining bytes available (0 when limit is reached)
+ */
+ remaining() {
+ return Math.max(0, this.maxSize - this.currentSize);
+ }
+
/**
* Gets the current accumulated size.
* @returns {number} Current size in bytes
From 49f67df18ab3013dbeed3e1af9a443fa2710bf63 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 5 Jul 2026 20:43:12 +0000
Subject: [PATCH 7/8] fix: clarify UTF-8 boundary check with named constants
and comment
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
actions/setup/js/log_parser_format.cjs | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/actions/setup/js/log_parser_format.cjs b/actions/setup/js/log_parser_format.cjs
index 2f38ba8e10d..c22c3a75819 100644
--- a/actions/setup/js/log_parser_format.cjs
+++ b/actions/setup/js/log_parser_format.cjs
@@ -169,9 +169,15 @@ function createLogParserFormatters(deps) {
}
// Truncate body at a clean UTF-8 character boundary.
+ // UTF-8 continuation bytes have the form 10xxxxxx (0x80ā0xBF).
+ // Walking back past them ensures the cutoff lands on a start byte
+ // (0x00ā0x7F for ASCII, 0xC0ā0xFF for multi-byte leaders), so the
+ // resulting slice is always a well-formed UTF-8 string.
+ const UTF8_CONTINUATION_MASK = 0xc0;
+ const UTF8_CONTINUATION_PREFIX = 0x80;
const bodyBuf = Buffer.from(body, "utf8");
let cutoff = Math.min(availableForBody, bodyBuf.length);
- while (cutoff > 0 && (bodyBuf[cutoff] & 0xc0) === 0x80) {
+ while (cutoff > 0 && (bodyBuf[cutoff] & UTF8_CONTINUATION_MASK) === UTF8_CONTINUATION_PREFIX) {
cutoff--;
}
const truncatedBody = bodyBuf.slice(0, cutoff).toString("utf8") + truncationNote;
From fefd13c9e6ea7eb4055050b67775bd0edaed3a60 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 6 Jul 2026 07:24:46 +0000
Subject: [PATCH 8/8] refactor: extract buildStepSummaryDetailsSection into
standalone helper file
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.../workflows/agentic-token-audit.lock.yml | 2 +-
.../agentic-token-optimizer.lock.yml | 2 +-
.../agentic-token-trend-audit.lock.yml | 2 +-
.../workflows/architecture-guardian.lock.yml | 2 +-
.github/workflows/auto-triage-issues.lock.yml | 2 +-
.../aw-failure-investigator.lock.yml | 2 +-
.github/workflows/ci-doctor.lock.yml | 2 +-
.../cli-consistency-checker.lock.yml | 2 +-
.github/workflows/code-simplifier.lock.yml | 2 +-
.github/workflows/contribution-check.lock.yml | 2 +-
.../copilot-centralization-drilldown.lock.yml | 2 +-
.../copilot-pr-nlp-analysis.lock.yml | 2 +-
.../daily-ambient-context-optimizer.lock.yml | 2 +-
.../daily-community-attribution.lock.yml | 2 +-
.../daily-multi-device-docs-tester.lock.yml | 2 +-
.../daily-security-observability.lock.yml | 2 +-
.../daily-spdd-spec-planner.lock.yml | 2 +-
.../dataflow-pr-discussion-dataset.lock.yml | 2 +-
.github/workflows/delight.lock.yml | 2 +-
.github/workflows/dependabot-burner.lock.yml | 2 +-
.../workflows/design-decision-gate.lock.yml | 2 +-
.../workflows/designer-drift-audit.lock.yml | 2 +-
.github/workflows/docs-noob-tester.lock.yml | 2 +-
.github/workflows/eslint-monster.lock.yml | 2 +-
.../workflows/glossary-maintainer.lock.yml | 2 +-
.github/workflows/gpclean.lock.yml | 2 +-
.../impeccable-skills-reviewer.lock.yml | 2 +-
.github/workflows/issue-arborist.lock.yml | 2 +-
.github/workflows/lint-monster.lock.yml | 2 +-
.github/workflows/linter-miner.lock.yml | 2 +-
.../mattpocock-skills-reviewer.lock.yml | 2 +-
.github/workflows/mergefest.lock.yml | 2 +-
.../objective-impact-report.lock.yml | 2 +-
.../workflows/pr-description-caveman.lock.yml | 2 +-
.../prompt-clustering-analysis.lock.yml | 2 +-
.github/workflows/release.lock.yml | 2 +-
.../repository-quality-improver.lock.yml | 2 +-
.../schema-consistency-checker.lock.yml | 2 +-
.github/workflows/spec-extractor.lock.yml | 2 +-
.../workflows/stale-repo-identifier.lock.yml | 2 +-
.../workflows/static-analysis-report.lock.yml | 2 +-
.../workflows/step-name-alignment.lock.yml | 2 +-
.../workflows/test-quality-sentinel.lock.yml | 2 +-
.../uk-ai-operational-resilience.lock.yml | 2 +-
.github/workflows/unbloat-docs.lock.yml | 2 +-
.../weekly-blog-post-writer.lock.yml | 2 +-
.../workflow-health-manager.lock.yml | 4 ++--
.../workflow-skill-extractor.lock.yml | 2 +-
actions/setup/js/log_parser_format.cjs | 23 ++-----------------
actions/setup/js/log_parser_shared.cjs | 17 +-------------
.../js/log_parser_step_summary_builder.cjs | 19 +++++++++++++++
51 files changed, 71 insertions(+), 86 deletions(-)
create mode 100644 actions/setup/js/log_parser_step_summary_builder.cjs
diff --git a/.github/workflows/agentic-token-audit.lock.yml b/.github/workflows/agentic-token-audit.lock.yml
index dfc3946ad98..de2da2d2e6a 100644
--- a/.github/workflows/agentic-token-audit.lock.yml
+++ b/.github/workflows/agentic-token-audit.lock.yml
@@ -525,7 +525,7 @@ jobs:
- env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Download agentic workflow logs
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/token-audit\n\n# Download last 24 hours of agentic workflow logs as JSON\n# Allow partial results ā gh aw logs streams incrementally, so even if\n# it hits an API rate limit partway through, the JSON written so far is\n# still valid and should be processed by the agent.\nLOGS_EXIT=0\ngh aw logs \\\n --start-date -1d \\\n --json \\\n -c 100 \\\n > /tmp/gh-aw/token-audit/workflow-logs.json || LOGS_EXIT=$?\n\nif [ -s /tmp/gh-aw/token-audit/workflow-logs.json ]; then\n TOTAL=$(jq '.runs | length' /tmp/gh-aw/token-audit/workflow-logs.json)\n echo \"ā
Downloaded $TOTAL agentic workflow runs (last 24 hours)\"\n if [ \"$LOGS_EXIT\" -ne 0 ]; then\n echo \"ā ļø gh aw logs exited with code $LOGS_EXIT (partial results ā likely API rate limit)\"\n fi\nelse\n echo \"ā No log data downloaded (exit code $LOGS_EXIT)\"\n echo '{\"runs\":[],\"summary\":{}}' > /tmp/gh-aw/token-audit/workflow-logs.json\nfi\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/token-audit\n\n# Download last 24 hours of agentic workflow logs as JSON\n# Allow partial results ā gh aw logs streams incrementally, so even if\n# it hits an API rate limit partway through, the JSON written so far is\n# still valid and should be processed by the agent.\nLOGS_EXIT=0\ngh aw logs \\\n --start-date -1d \\\n --json \\\n -c 100 \\\n > /tmp/gh-aw/token-audit/workflow-logs.json || LOGS_EXIT=$?\n\nif [ -s /tmp/gh-aw/token-audit/workflow-logs.json ]; then\n TOTAL=$(jq '.runs | length' /tmp/gh-aw/token-audit/workflow-logs.json)\n echo \"ā
Downloaded $TOTAL agentic workflow runs (last 24 hours)\"\n if [ \"$LOGS_EXIT\" -ne 0 ]; then\n echo \"ā ļø gh aw logs exited with code $LOGS_EXIT (partial results ā likely API rate limit)\"\n fi\nelse\n echo \"ā No log data downloaded (exit code $LOGS_EXIT)\"\n echo '{\"runs\":[],\"summary\":{}}' > /tmp/gh-aw/token-audit/workflow-logs.json\nfi"
# Repo memory git-based storage configuration from frontmatter processed below
- name: Clone repo-memory branch (default)
diff --git a/.github/workflows/agentic-token-optimizer.lock.yml b/.github/workflows/agentic-token-optimizer.lock.yml
index 248cace1314..3b6906eb720 100644
--- a/.github/workflows/agentic-token-optimizer.lock.yml
+++ b/.github/workflows/agentic-token-optimizer.lock.yml
@@ -483,7 +483,7 @@ jobs:
- name: Aggregate top workflows by AIC usage
run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/token-audit\n\njq '{\n generated_at: (now | todateiso8601),\n window_days: 7,\n top_workflows: (\n [.runs[]\n | select(.status == \"completed\")\n | {\n workflow_name: .workflow_name,\n aic: (.aic // 0),\n raw_tokens: (.token_usage // 0),\n turns: (.turns // 0),\n action_minutes: (.action_minutes // 0)\n }\n ]\n | group_by(.workflow_name)\n | map({\n workflow_name: .[0].workflow_name,\n run_count: length,\n total_aic: (map(.aic) | add),\n avg_aic: ((map(.aic) | add) / length),\n total_raw_tokens: (map(.raw_tokens) | add),\n total_turns: (map(.turns) | add),\n total_action_minutes: (map(.action_minutes) | add)\n })\n | sort_by(.total_aic)\n | reverse\n | .[:10]\n )\n}' /tmp/gh-aw/token-audit/all-runs.json > /tmp/gh-aw/token-audit/top-workflows.json\n\necho \"ā
Generated top workflow summary at /tmp/gh-aw/token-audit/top-workflows.json\"\njq '.top_workflows' /tmp/gh-aw/token-audit/top-workflows.json\n"
- name: Load optimization history
- run: "set -euo pipefail\n\nOPT_LOG=\"/tmp/gh-aw/repo-memory/default/optimization-log.json\"\nif [ -f \"$OPT_LOG\" ]; then\n echo \"ā
Previous optimizations:\"\n jq -r '.[] | \"\\(.date): \\(.workflow_name)\"' \"$OPT_LOG\"\nelse\n echo \"ā¹ļø No previous optimization history found.\"\nfi\n"
+ run: "set -euo pipefail\n\nOPT_LOG=\"/tmp/gh-aw/repo-memory/default/optimization-log.json\"\nif [ -f \"$OPT_LOG\" ]; then\n echo \"ā
Previous optimizations:\"\n jq -r '.[] | \"\\(.date): \\(.workflow_name)\"' \"$OPT_LOG\"\nelse\n echo \"ā¹ļø No previous optimization history found.\"\nfi"
# Repo memory git-based storage configuration from frontmatter processed below
- name: Clone repo-memory branch (default)
diff --git a/.github/workflows/agentic-token-trend-audit.lock.yml b/.github/workflows/agentic-token-trend-audit.lock.yml
index f040ef04ab9..fbf66fe3cc4 100644
--- a/.github/workflows/agentic-token-trend-audit.lock.yml
+++ b/.github/workflows/agentic-token-trend-audit.lock.yml
@@ -509,7 +509,7 @@ jobs:
DATE_RANGE_INPUT: ${{ github.event.inputs.date_range }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Download agentic workflow logs
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/token-audit\n\nDATE_RANGE=\"$DATE_RANGE_INPUT\"\nif [[ \"$DATE_RANGE\" != *\"..\"* ]]; then\n echo \"ā Invalid date_range input: $DATE_RANGE\"\n echo \"Expected format: .. (for example 2026-05-01..2026-05-31 or -30d..now)\"\n exit 1\nfi\n\nSTART_DATE=\"${DATE_RANGE%%..*}\"\nEND_DATE=\"${DATE_RANGE##*..}\"\n\n# Download logs for the requested range as JSON.\n# Allow partial results ā gh aw logs streams incrementally, so even if\n# it hits an API rate limit partway through, the JSON written so far is\n# still valid and should be processed by the agent.\nLOGS_EXIT=0\ngh aw logs \\\n --start-date \"$START_DATE\" \\\n --end-date \"$END_DATE\" \\\n --json \\\n -c 500 \\\n > /tmp/gh-aw/token-audit/workflow-logs.json || LOGS_EXIT=$?\n\nif [ -s /tmp/gh-aw/token-audit/workflow-logs.json ]; then\n TOTAL=$(jq '.runs | length' /tmp/gh-aw/token-audit/workflow-logs.json)\n echo \"ā
Downloaded $TOTAL agentic workflow runs ($START_DATE to $END_DATE)\"\n if [ \"$LOGS_EXIT\" -ne 0 ]; then\n echo \"ā ļø gh aw logs exited with code $LOGS_EXIT (partial results ā likely API rate limit)\"\n fi\nelse\n echo \"ā No log data downloaded (exit code $LOGS_EXIT)\"\n echo '{\"runs\":[],\"summary\":{}}' > /tmp/gh-aw/token-audit/workflow-logs.json\nfi\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/token-audit\n\nDATE_RANGE=\"$DATE_RANGE_INPUT\"\nif [[ \"$DATE_RANGE\" != *\"..\"* ]]; then\n echo \"ā Invalid date_range input: $DATE_RANGE\"\n echo \"Expected format: .. (for example 2026-05-01..2026-05-31 or -30d..now)\"\n exit 1\nfi\n\nSTART_DATE=\"${DATE_RANGE%%..*}\"\nEND_DATE=\"${DATE_RANGE##*..}\"\n\n# Download logs for the requested range as JSON.\n# Allow partial results ā gh aw logs streams incrementally, so even if\n# it hits an API rate limit partway through, the JSON written so far is\n# still valid and should be processed by the agent.\nLOGS_EXIT=0\ngh aw logs \\\n --start-date \"$START_DATE\" \\\n --end-date \"$END_DATE\" \\\n --json \\\n -c 500 \\\n > /tmp/gh-aw/token-audit/workflow-logs.json || LOGS_EXIT=$?\n\nif [ -s /tmp/gh-aw/token-audit/workflow-logs.json ]; then\n TOTAL=$(jq '.runs | length' /tmp/gh-aw/token-audit/workflow-logs.json)\n echo \"ā
Downloaded $TOTAL agentic workflow runs ($START_DATE to $END_DATE)\"\n if [ \"$LOGS_EXIT\" -ne 0 ]; then\n echo \"ā ļø gh aw logs exited with code $LOGS_EXIT (partial results ā likely API rate limit)\"\n fi\nelse\n echo \"ā No log data downloaded (exit code $LOGS_EXIT)\"\n echo '{\"runs\":[],\"summary\":{}}' > /tmp/gh-aw/token-audit/workflow-logs.json\nfi"
- name: Configure Git credentials
env:
diff --git a/.github/workflows/architecture-guardian.lock.yml b/.github/workflows/architecture-guardian.lock.yml
index ad86986d5e6..6b51ec42799 100644
--- a/.github/workflows/architecture-guardian.lock.yml
+++ b/.github/workflows/architecture-guardian.lock.yml
@@ -526,7 +526,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Collect architecture metrics
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\n# Read thresholds from .architecture.yml or use defaults\nFILE_LINES_BLOCKER=1000\nFILE_LINES_WARNING=500\nFUNCTION_LINES=80\nMAX_EXPORTS=10\n\nif [ -f .architecture.yml ]; then\n b=$(grep -E '^\\s*file_lines_blocker:' .architecture.yml 2>/dev/null | awk '{print $2}' | tr -d '\"' | head -1 || true)\n w=$(grep -E '^\\s*file_lines_warning:' .architecture.yml 2>/dev/null | awk '{print $2}' | tr -d '\"' | head -1 || true)\n f=$(grep -E '^\\s*function_lines:' .architecture.yml 2>/dev/null | awk '{print $2}' | tr -d '\"' | head -1 || true)\n e=$(grep -E '^\\s*max_exports:' .architecture.yml 2>/dev/null | awk '{print $2}' | tr -d '\"' | head -1 || true)\n [[ -n \"${b:-}\" && \"$b\" =~ ^[0-9]+$ ]] && FILE_LINES_BLOCKER=$b\n [[ -n \"${w:-}\" && \"$w\" =~ ^[0-9]+$ ]] && FILE_LINES_WARNING=$w\n [[ -n \"${f:-}\" && \"$f\" =~ ^[0-9]+$ ]] && FUNCTION_LINES=$f\n [[ -n \"${e:-}\" && \"$e\" =~ ^[0-9]+$ ]] && MAX_EXPORTS=$e\nfi\n\n# Get changed Go/JS files in last 24 hours, excluding tests and vendor paths\nCHANGED_FILES=$(git log --since=\"24 hours ago\" --name-only --pretty=format: \\\n | sort -u \\\n | grep -E '\\.(go|js|cjs|mjs)$' \\\n | grep -vE '(node_modules/|vendor/|\\.git/|_test\\.go$)' \\\n | while IFS= read -r f; do [ -f \"$f\" ] && echo \"$f\"; done \\\n || true)\n\nif [ -z \"$CHANGED_FILES\" ]; then\n jq -n \\\n --argjson blocker \"$FILE_LINES_BLOCKER\" \\\n --argjson warning \"$FILE_LINES_WARNING\" \\\n --argjson func_lines \"$FUNCTION_LINES\" \\\n --argjson max_exports \"$MAX_EXPORTS\" \\\n '{noop: true, thresholds: {file_lines_blocker: $blocker, file_lines_warning: $warning, function_lines: $func_lines, max_exports: $max_exports}, files: [], import_cycles: \"\"}' \\\n > /tmp/gh-aw/agent/arch-metrics.json\n echo \"No changed Go/JS files found in the last 24 hours.\"\n exit 0\nfi\n\n# Build file metrics array\nFILES_JSON=\"[]\"\nwhile IFS= read -r FILE; do\n [ -z \"$FILE\" ] && continue\n LINES=$(wc -l < \"$FILE\" 2>/dev/null | tr -d ' ' || echo 0)\n EXT=\"${FILE##*.}\"\n\n if [[ \"$EXT\" == \"go\" ]]; then\n # Function sizes: \"func declaration\\tline_count\" per function\n # Pattern matches both regular functions (^func Name) and receiver methods (^func (r *T) Name)\n FUNC_DATA=$(awk '/^func /{if(start>0 && name!=\"\") printf \"%s\\t%d\\n\", name, NR-start; name=$0; start=NR} END{if(start>0 && name!=\"\") printf \"%s\\t%d\\n\", name, NR-start+1}' \"$FILE\" 2>/dev/null | head -50 || true)\n # Export count and names (top-level exported identifiers start with uppercase)\n EXPORT_COUNT=$(grep -cE \"^func [A-Z]|^type [A-Z]|^var [A-Z]|^const [A-Z]\" \"$FILE\" 2>/dev/null || echo 0)\n EXPORT_NAMES=$(grep -nE \"^func [A-Z]|^type [A-Z]|^var [A-Z]|^const [A-Z]\" \"$FILE\" 2>/dev/null | head -20 || true)\n else\n # JS/CJS/MJS: capture named functions, arrow functions, and class methods\n FUNC_DATA=$(grep -nE \"^function |^const [a-zA-Z_$][a-zA-Z0-9_$]* = (function|\\(|async \\(|async function)|^(export (default )?function|export const [a-zA-Z_$][a-zA-Z0-9_$]* =)|^[a-zA-Z_$][a-zA-Z0-9_$]*\\s*\\([^)]*\\)\\s*\\{\" \"$FILE\" 2>/dev/null | head -50 || true)\n CJS_COUNT=$(grep -cE \"^module\\.exports|^exports\\.\" \"$FILE\" 2>/dev/null || echo 0)\n ESM_COUNT=$(grep -cE \"^export \" \"$FILE\" 2>/dev/null || echo 0)\n EXPORT_COUNT=$((CJS_COUNT + ESM_COUNT))\n EXPORT_NAMES=$(grep -nE \"^export |^module\\.exports|^exports\\.\" \"$FILE\" 2>/dev/null | head -20 || true)\n fi\n\n FILES_JSON=$(jq \\\n --arg file \"$FILE\" \\\n --argjson lines \"$LINES\" \\\n --argjson exports \"$EXPORT_COUNT\" \\\n --arg func_data \"${FUNC_DATA:-}\" \\\n --arg export_names \"${EXPORT_NAMES:-}\" \\\n '. + [{file: $file, lines: $lines, export_count: $exports, func_data: $func_data, export_names: $export_names}]' \\\n <<< \"$FILES_JSON\")\ndone <<< \"$CHANGED_FILES\"\n\n# Check Go import cycles once across all packages\n# Note: go list may also emit errors for syntax issues; grep filters to only cycle errors\nIMPORT_CYCLES=$(go list ./... 2>&1 | grep -iE \"import cycle|cycle not allowed\" || true)\n\njq -n \\\n --argjson blocker \"$FILE_LINES_BLOCKER\" \\\n --argjson warning \"$FILE_LINES_WARNING\" \\\n --argjson func_lines \"$FUNCTION_LINES\" \\\n --argjson max_exports \"$MAX_EXPORTS\" \\\n --argjson files \"$FILES_JSON\" \\\n --arg import_cycles \"$IMPORT_CYCLES\" \\\n '{noop: false, thresholds: {file_lines_blocker: $blocker, file_lines_warning: $warning, function_lines: $func_lines, max_exports: $max_exports}, files: $files, import_cycles: $import_cycles}' \\\n > /tmp/gh-aw/agent/arch-metrics.json\n\nFILE_COUNT=$(echo \"$CHANGED_FILES\" | wc -l | tr -d ' ')\necho \"ā
Pre-computed metrics for $FILE_COUNT file(s) ā /tmp/gh-aw/agent/arch-metrics.json\"\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\n# Read thresholds from .architecture.yml or use defaults\nFILE_LINES_BLOCKER=1000\nFILE_LINES_WARNING=500\nFUNCTION_LINES=80\nMAX_EXPORTS=10\n\nif [ -f .architecture.yml ]; then\n b=$(grep -E '^\\s*file_lines_blocker:' .architecture.yml 2>/dev/null | awk '{print $2}' | tr -d '\"' | head -1 || true)\n w=$(grep -E '^\\s*file_lines_warning:' .architecture.yml 2>/dev/null | awk '{print $2}' | tr -d '\"' | head -1 || true)\n f=$(grep -E '^\\s*function_lines:' .architecture.yml 2>/dev/null | awk '{print $2}' | tr -d '\"' | head -1 || true)\n e=$(grep -E '^\\s*max_exports:' .architecture.yml 2>/dev/null | awk '{print $2}' | tr -d '\"' | head -1 || true)\n [[ -n \"${b:-}\" && \"$b\" =~ ^[0-9]+$ ]] && FILE_LINES_BLOCKER=$b\n [[ -n \"${w:-}\" && \"$w\" =~ ^[0-9]+$ ]] && FILE_LINES_WARNING=$w\n [[ -n \"${f:-}\" && \"$f\" =~ ^[0-9]+$ ]] && FUNCTION_LINES=$f\n [[ -n \"${e:-}\" && \"$e\" =~ ^[0-9]+$ ]] && MAX_EXPORTS=$e\nfi\n\n# Get changed Go/JS files in last 24 hours, excluding tests and vendor paths\nCHANGED_FILES=$(git log --since=\"24 hours ago\" --name-only --pretty=format: \\\n | sort -u \\\n | grep -E '\\.(go|js|cjs|mjs)$' \\\n | grep -vE '(node_modules/|vendor/|\\.git/|_test\\.go$)' \\\n | while IFS= read -r f; do [ -f \"$f\" ] && echo \"$f\"; done \\\n || true)\n\nif [ -z \"$CHANGED_FILES\" ]; then\n jq -n \\\n --argjson blocker \"$FILE_LINES_BLOCKER\" \\\n --argjson warning \"$FILE_LINES_WARNING\" \\\n --argjson func_lines \"$FUNCTION_LINES\" \\\n --argjson max_exports \"$MAX_EXPORTS\" \\\n '{noop: true, thresholds: {file_lines_blocker: $blocker, file_lines_warning: $warning, function_lines: $func_lines, max_exports: $max_exports}, files: [], import_cycles: \"\"}' \\\n > /tmp/gh-aw/agent/arch-metrics.json\n echo \"No changed Go/JS files found in the last 24 hours.\"\n exit 0\nfi\n\n# Build file metrics array\nFILES_JSON=\"[]\"\nwhile IFS= read -r FILE; do\n [ -z \"$FILE\" ] && continue\n LINES=$(wc -l < \"$FILE\" 2>/dev/null | tr -d ' ' || echo 0)\n EXT=\"${FILE##*.}\"\n\n if [[ \"$EXT\" == \"go\" ]]; then\n # Function sizes: \"func declaration\\tline_count\" per function\n # Pattern matches both regular functions (^func Name) and receiver methods (^func (r *T) Name)\n FUNC_DATA=$(awk '/^func /{if(start>0 && name!=\"\") printf \"%s\\t%d\\n\", name, NR-start; name=$0; start=NR} END{if(start>0 && name!=\"\") printf \"%s\\t%d\\n\", name, NR-start+1}' \"$FILE\" 2>/dev/null | head -50 || true)\n # Export count and names (top-level exported identifiers start with uppercase)\n EXPORT_COUNT=$(grep -cE \"^func [A-Z]|^type [A-Z]|^var [A-Z]|^const [A-Z]\" \"$FILE\" 2>/dev/null || echo 0)\n EXPORT_NAMES=$(grep -nE \"^func [A-Z]|^type [A-Z]|^var [A-Z]|^const [A-Z]\" \"$FILE\" 2>/dev/null | head -20 || true)\n else\n # JS/CJS/MJS: capture named functions, arrow functions, and class methods\n FUNC_DATA=$(grep -nE \"^function |^const [a-zA-Z_$][a-zA-Z0-9_$]* = (function|\\(|async \\(|async function)|^(export (default )?function|export const [a-zA-Z_$][a-zA-Z0-9_$]* =)|^[a-zA-Z_$][a-zA-Z0-9_$]*\\s*\\([^)]*\\)\\s*\\{\" \"$FILE\" 2>/dev/null | head -50 || true)\n CJS_COUNT=$(grep -cE \"^module\\.exports|^exports\\.\" \"$FILE\" 2>/dev/null || echo 0)\n ESM_COUNT=$(grep -cE \"^export \" \"$FILE\" 2>/dev/null || echo 0)\n EXPORT_COUNT=$((CJS_COUNT + ESM_COUNT))\n EXPORT_NAMES=$(grep -nE \"^export |^module\\.exports|^exports\\.\" \"$FILE\" 2>/dev/null | head -20 || true)\n fi\n\n FILES_JSON=$(jq \\\n --arg file \"$FILE\" \\\n --argjson lines \"$LINES\" \\\n --argjson exports \"$EXPORT_COUNT\" \\\n --arg func_data \"${FUNC_DATA:-}\" \\\n --arg export_names \"${EXPORT_NAMES:-}\" \\\n '. + [{file: $file, lines: $lines, export_count: $exports, func_data: $func_data, export_names: $export_names}]' \\\n <<< \"$FILES_JSON\")\ndone <<< \"$CHANGED_FILES\"\n\n# Check Go import cycles once across all packages\n# Note: go list may also emit errors for syntax issues; grep filters to only cycle errors\nIMPORT_CYCLES=$(go list ./... 2>&1 | grep -iE \"import cycle|cycle not allowed\" || true)\n\njq -n \\\n --argjson blocker \"$FILE_LINES_BLOCKER\" \\\n --argjson warning \"$FILE_LINES_WARNING\" \\\n --argjson func_lines \"$FUNCTION_LINES\" \\\n --argjson max_exports \"$MAX_EXPORTS\" \\\n --argjson files \"$FILES_JSON\" \\\n --arg import_cycles \"$IMPORT_CYCLES\" \\\n '{noop: false, thresholds: {file_lines_blocker: $blocker, file_lines_warning: $warning, function_lines: $func_lines, max_exports: $max_exports}, files: $files, import_cycles: $import_cycles}' \\\n > /tmp/gh-aw/agent/arch-metrics.json\n\nFILE_COUNT=$(echo \"$CHANGED_FILES\" | wc -l | tr -d ' ')\necho \"ā
Pre-computed metrics for $FILE_COUNT file(s) ā /tmp/gh-aw/agent/arch-metrics.json\""
- name: Configure Git credentials
env:
diff --git a/.github/workflows/auto-triage-issues.lock.yml b/.github/workflows/auto-triage-issues.lock.yml
index 69a39074b87..64ed523e7b4 100644
--- a/.github/workflows/auto-triage-issues.lock.yml
+++ b/.github/workflows/auto-triage-issues.lock.yml
@@ -499,7 +499,7 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/start_difc_proxy.sh"
- name: Fetch unlabeled issues
- run: |
+ run: |-
mkdir -p /tmp/gh-aw/agent
gh api "repos/github/gh-aw/issues?state=open&labels=&per_page=30" \
--jq '[.[] | select(.labels | length == 0) | {number: .number, title: .title, body: .body}]' \
diff --git a/.github/workflows/aw-failure-investigator.lock.yml b/.github/workflows/aw-failure-investigator.lock.yml
index acdf9ef612f..16c18f61439 100644
--- a/.github/workflows/aw-failure-investigator.lock.yml
+++ b/.github/workflows/aw-failure-investigator.lock.yml
@@ -556,7 +556,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Deterministic pre-fetch for failure analysis
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent/failure-investigator\npython3 - <<'PY'\nimport json\nimport os\nimport subprocess\nfrom datetime import datetime, timedelta, timezone\nfrom pathlib import Path\nfrom urllib.parse import urlencode\n\nREPO = os.environ[\"GITHUB_REPOSITORY\"]\nOUT = \"/tmp/gh-aw/agent/failure-investigator/prefetch.json\"\nTRACKER_ID = \"aw-failure-investigator\"\nLOOKBACK_HOURS = 6\nFAILURE_CONCLUSIONS = {\"failure\", \"timed_out\", \"startup_failure\", \"cancelled\"}\nMAX_DISCOVERY_PAGES = 20\n# Most dominant signatures appear in the final 30-60 lines.\nMAX_LOG_TAIL_LINES = 50\n# Deep-dive budget: investigate at most this many distinct failed runs.\nMAX_FAILURES_TO_DETAIL = 5\nAGENTIC_WORKFLOW_PATHS = {\n f\".github/workflows/{path.name}\"\n for path in Path(\".github/workflows\").glob(\"*.lock.yml\")\n}\n\ndef cmd_display(args):\n return \" \".join(args)\n\ndef run_json(args):\n try:\n out = subprocess.check_output(args, text=True, stderr=subprocess.STDOUT)\n return json.loads(out)\n except subprocess.CalledProcessError as error:\n print(f\"Warning: command failed: {cmd_display(args)}\")\n print(error.output)\n return None\n except json.JSONDecodeError as error:\n print(f\"Warning: non-JSON output from command: {cmd_display(args)} ({error})\")\n return None\n except OSError as error:\n print(f\"Warning: could not execute command: {cmd_display(args)} ({error})\")\n return None\n\ndef run_text(args):\n try:\n return subprocess.check_output(args, text=True, stderr=subprocess.STDOUT)\n except subprocess.CalledProcessError as error:\n print(f\"Warning: command failed: {cmd_display(args)}\")\n print(error.output)\n return \"\"\n except OSError as error:\n print(f\"Warning: could not execute command: {cmd_display(args)} ({error})\")\n return \"\"\n\ndef run_api_json(endpoint, params):\n query = urlencode(params)\n return run_json([\"gh\", \"api\", f\"{endpoint}?{query}\"])\n\ndef is_failure_conclusion(conclusion):\n return (conclusion or \"\").lower() in FAILURE_CONCLUSIONS\n\ndef normalize_workflow_path(path):\n return (path or \"\").split(\"@\", 1)[0]\n\ndef is_agentic_workflow_path(path):\n workflow_path = normalize_workflow_path(path)\n if AGENTIC_WORKFLOW_PATHS:\n return workflow_path in AGENTIC_WORKFLOW_PATHS\n print(\"Warning: no local .lock.yml workflows found; falling back to workflow path suffix matching\")\n return workflow_path.endswith(\".lock.yml\")\n\ndef isoformat_z(dt):\n return dt.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace(\"+00:00\", \"Z\")\n\ndef list_failed_agentic_runs():\n created_since = isoformat_z(datetime.now(timezone.utc) - timedelta(hours=LOOKBACK_HOURS))\n page = 1\n failed_runs = []\n\n while True:\n response = run_api_json(\n f\"repos/{REPO}/actions/runs\",\n {\n \"exclude_pull_requests\": \"true\",\n \"status\": \"completed\",\n \"created\": f\">={created_since}\",\n \"per_page\": \"100\",\n \"page\": str(page),\n },\n ) or {}\n workflow_runs = response.get(\"workflow_runs\") or []\n if not workflow_runs:\n break\n\n for run in workflow_runs:\n workflow_path = normalize_workflow_path(run.get(\"path\"))\n if not is_agentic_workflow_path(workflow_path):\n continue\n if not is_failure_conclusion(run.get(\"conclusion\")):\n continue\n\n failed_runs.append(\n {\n \"run_id\": run.get(\"id\"),\n \"workflow_name\": run.get(\"name\"),\n \"workflow_path\": workflow_path,\n \"created_at\": run.get(\"created_at\"),\n \"status\": run.get(\"status\"),\n \"conclusion\": run.get(\"conclusion\"),\n \"url\": run.get(\"html_url\"),\n }\n )\n\n if len(workflow_runs) < 100:\n break\n if page >= MAX_DISCOVERY_PAGES:\n print(f\"Warning: reached pagination cap ({MAX_DISCOVERY_PAGES} pages) while listing workflow runs\")\n break\n page += 1\n\n failed_runs.sort(key=lambda run: run.get(\"created_at\") or \"\", reverse=True)\n return failed_runs\n\nfailed_runs = list_failed_agentic_runs()\n\n# Cap the number of runs to detail so the payload stays compact.\nfailure_details = []\nfor run in failed_runs[:MAX_FAILURES_TO_DETAIL]:\n run_id = run.get(\"run_id\")\n if not run_id:\n continue\n\n run_view = run_json(\n [\n \"gh\",\n \"run\",\n \"view\",\n str(run_id),\n \"--repo\",\n REPO,\n \"--json\",\n \"databaseId,url,name,workflowName,createdAt,conclusion,status,jobs\",\n ]\n )\n if not run_view:\n continue\n\n failed_job_names = []\n failed_steps = []\n truncated_error_logs = []\n agent_job_conclusion = None\n for job in run_view.get(\"jobs\", []):\n job_name = job.get(\"name\")\n job_conclusion = (job.get(\"conclusion\") or \"\").lower()\n if (job_name or \"\").lower() == \"agent\":\n agent_job_conclusion = job_conclusion or None\n\n if is_failure_conclusion(job_conclusion):\n if job_name:\n failed_job_names.append(job_name)\n for step in job.get(\"steps\", []):\n if is_failure_conclusion(step.get(\"conclusion\")):\n failed_steps.append(\n {\n \"job_id\": job.get(\"databaseId\"),\n \"job_name\": job_name,\n \"step_name\": step.get(\"name\"),\n }\n )\n\n job_id = job.get(\"databaseId\")\n if job_id:\n log_text = run_text(\n [\n \"gh\",\n \"run\",\n \"view\",\n str(run_id),\n \"--repo\",\n REPO,\n \"--job\",\n str(job_id),\n \"--log-failed\",\n ]\n )\n if log_text:\n tail_lines = log_text.splitlines()[-MAX_LOG_TAIL_LINES:]\n truncated_error_logs.append(\n {\n \"job_id\": job_id,\n \"job_name\": job_name,\n \"line_count\": len(tail_lines),\n \"tail_lines\": \"\\n\".join(tail_lines),\n }\n )\n\n failure_details.append(\n {\n \"run_id\": run_id,\n \"workflow_name\": run_view.get(\"workflowName\") or run_view.get(\"name\"),\n \"workflow_path\": run.get(\"workflow_path\"),\n \"url\": run_view.get(\"url\"),\n \"created_at\": run_view.get(\"createdAt\"),\n \"status\": run_view.get(\"status\"),\n \"conclusion\": run_view.get(\"conclusion\"),\n \"failed_job_names\": sorted(set(failed_job_names)),\n \"agent_job_conclusion\": agent_job_conclusion,\n \"failed_steps\": failed_steps,\n \"truncated_error_logs\": truncated_error_logs,\n }\n )\n\nexisting_tracking_issues = run_json(\n [\n \"gh\",\n \"issue\",\n \"list\",\n \"--repo\",\n REPO,\n \"--state\",\n \"open\",\n \"--search\",\n f\"gh-aw-tracker-id: {TRACKER_ID}\",\n \"--limit\",\n \"100\",\n \"--json\",\n \"number,title,state,url,labels,createdAt,updatedAt\",\n ]\n) or []\n\npayload = {\n \"generated_at\": datetime.now(timezone.utc).isoformat(),\n \"repository\": REPO,\n \"lookback_window\": f\"{LOOKBACK_HOURS}h\",\n \"failed_run_ids\": [run.get(\"run_id\") for run in failed_runs if run.get(\"run_id\")],\n \"failures\": failure_details,\n \"existing_tracking_issues\": existing_tracking_issues,\n}\n\nwith open(OUT, \"w\", encoding=\"utf-8\") as f:\n json.dump(payload, f, indent=2)\n f.write(\"\\n\")\n\nprint(f\"Wrote deterministic prefetch payload to {OUT}\")\nprint(f\"Failed runs in payload: {len(payload['failed_run_ids'])}\")\nprint(f\"Existing tracking issues in payload: {len(existing_tracking_issues)}\")\nPY\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent/failure-investigator\npython3 - <<'PY'\nimport json\nimport os\nimport subprocess\nfrom datetime import datetime, timedelta, timezone\nfrom pathlib import Path\nfrom urllib.parse import urlencode\n\nREPO = os.environ[\"GITHUB_REPOSITORY\"]\nOUT = \"/tmp/gh-aw/agent/failure-investigator/prefetch.json\"\nTRACKER_ID = \"aw-failure-investigator\"\nLOOKBACK_HOURS = 6\nFAILURE_CONCLUSIONS = {\"failure\", \"timed_out\", \"startup_failure\", \"cancelled\"}\nMAX_DISCOVERY_PAGES = 20\n# Most dominant signatures appear in the final 30-60 lines.\nMAX_LOG_TAIL_LINES = 50\n# Deep-dive budget: investigate at most this many distinct failed runs.\nMAX_FAILURES_TO_DETAIL = 5\nAGENTIC_WORKFLOW_PATHS = {\n f\".github/workflows/{path.name}\"\n for path in Path(\".github/workflows\").glob(\"*.lock.yml\")\n}\n\ndef cmd_display(args):\n return \" \".join(args)\n\ndef run_json(args):\n try:\n out = subprocess.check_output(args, text=True, stderr=subprocess.STDOUT)\n return json.loads(out)\n except subprocess.CalledProcessError as error:\n print(f\"Warning: command failed: {cmd_display(args)}\")\n print(error.output)\n return None\n except json.JSONDecodeError as error:\n print(f\"Warning: non-JSON output from command: {cmd_display(args)} ({error})\")\n return None\n except OSError as error:\n print(f\"Warning: could not execute command: {cmd_display(args)} ({error})\")\n return None\n\ndef run_text(args):\n try:\n return subprocess.check_output(args, text=True, stderr=subprocess.STDOUT)\n except subprocess.CalledProcessError as error:\n print(f\"Warning: command failed: {cmd_display(args)}\")\n print(error.output)\n return \"\"\n except OSError as error:\n print(f\"Warning: could not execute command: {cmd_display(args)} ({error})\")\n return \"\"\n\ndef run_api_json(endpoint, params):\n query = urlencode(params)\n return run_json([\"gh\", \"api\", f\"{endpoint}?{query}\"])\n\ndef is_failure_conclusion(conclusion):\n return (conclusion or \"\").lower() in FAILURE_CONCLUSIONS\n\ndef normalize_workflow_path(path):\n return (path or \"\").split(\"@\", 1)[0]\n\ndef is_agentic_workflow_path(path):\n workflow_path = normalize_workflow_path(path)\n if AGENTIC_WORKFLOW_PATHS:\n return workflow_path in AGENTIC_WORKFLOW_PATHS\n print(\"Warning: no local .lock.yml workflows found; falling back to workflow path suffix matching\")\n return workflow_path.endswith(\".lock.yml\")\n\ndef isoformat_z(dt):\n return dt.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace(\"+00:00\", \"Z\")\n\ndef list_failed_agentic_runs():\n created_since = isoformat_z(datetime.now(timezone.utc) - timedelta(hours=LOOKBACK_HOURS))\n page = 1\n failed_runs = []\n\n while True:\n response = run_api_json(\n f\"repos/{REPO}/actions/runs\",\n {\n \"exclude_pull_requests\": \"true\",\n \"status\": \"completed\",\n \"created\": f\">={created_since}\",\n \"per_page\": \"100\",\n \"page\": str(page),\n },\n ) or {}\n workflow_runs = response.get(\"workflow_runs\") or []\n if not workflow_runs:\n break\n\n for run in workflow_runs:\n workflow_path = normalize_workflow_path(run.get(\"path\"))\n if not is_agentic_workflow_path(workflow_path):\n continue\n if not is_failure_conclusion(run.get(\"conclusion\")):\n continue\n\n failed_runs.append(\n {\n \"run_id\": run.get(\"id\"),\n \"workflow_name\": run.get(\"name\"),\n \"workflow_path\": workflow_path,\n \"created_at\": run.get(\"created_at\"),\n \"status\": run.get(\"status\"),\n \"conclusion\": run.get(\"conclusion\"),\n \"url\": run.get(\"html_url\"),\n }\n )\n\n if len(workflow_runs) < 100:\n break\n if page >= MAX_DISCOVERY_PAGES:\n print(f\"Warning: reached pagination cap ({MAX_DISCOVERY_PAGES} pages) while listing workflow runs\")\n break\n page += 1\n\n failed_runs.sort(key=lambda run: run.get(\"created_at\") or \"\", reverse=True)\n return failed_runs\n\nfailed_runs = list_failed_agentic_runs()\n\n# Cap the number of runs to detail so the payload stays compact.\nfailure_details = []\nfor run in failed_runs[:MAX_FAILURES_TO_DETAIL]:\n run_id = run.get(\"run_id\")\n if not run_id:\n continue\n\n run_view = run_json(\n [\n \"gh\",\n \"run\",\n \"view\",\n str(run_id),\n \"--repo\",\n REPO,\n \"--json\",\n \"databaseId,url,name,workflowName,createdAt,conclusion,status,jobs\",\n ]\n )\n if not run_view:\n continue\n\n failed_job_names = []\n failed_steps = []\n truncated_error_logs = []\n agent_job_conclusion = None\n for job in run_view.get(\"jobs\", []):\n job_name = job.get(\"name\")\n job_conclusion = (job.get(\"conclusion\") or \"\").lower()\n if (job_name or \"\").lower() == \"agent\":\n agent_job_conclusion = job_conclusion or None\n\n if is_failure_conclusion(job_conclusion):\n if job_name:\n failed_job_names.append(job_name)\n for step in job.get(\"steps\", []):\n if is_failure_conclusion(step.get(\"conclusion\")):\n failed_steps.append(\n {\n \"job_id\": job.get(\"databaseId\"),\n \"job_name\": job_name,\n \"step_name\": step.get(\"name\"),\n }\n )\n\n job_id = job.get(\"databaseId\")\n if job_id:\n log_text = run_text(\n [\n \"gh\",\n \"run\",\n \"view\",\n str(run_id),\n \"--repo\",\n REPO,\n \"--job\",\n str(job_id),\n \"--log-failed\",\n ]\n )\n if log_text:\n tail_lines = log_text.splitlines()[-MAX_LOG_TAIL_LINES:]\n truncated_error_logs.append(\n {\n \"job_id\": job_id,\n \"job_name\": job_name,\n \"line_count\": len(tail_lines),\n \"tail_lines\": \"\\n\".join(tail_lines),\n }\n )\n\n failure_details.append(\n {\n \"run_id\": run_id,\n \"workflow_name\": run_view.get(\"workflowName\") or run_view.get(\"name\"),\n \"workflow_path\": run.get(\"workflow_path\"),\n \"url\": run_view.get(\"url\"),\n \"created_at\": run_view.get(\"createdAt\"),\n \"status\": run_view.get(\"status\"),\n \"conclusion\": run_view.get(\"conclusion\"),\n \"failed_job_names\": sorted(set(failed_job_names)),\n \"agent_job_conclusion\": agent_job_conclusion,\n \"failed_steps\": failed_steps,\n \"truncated_error_logs\": truncated_error_logs,\n }\n )\n\nexisting_tracking_issues = run_json(\n [\n \"gh\",\n \"issue\",\n \"list\",\n \"--repo\",\n REPO,\n \"--state\",\n \"open\",\n \"--search\",\n f\"gh-aw-tracker-id: {TRACKER_ID}\",\n \"--limit\",\n \"100\",\n \"--json\",\n \"number,title,state,url,labels,createdAt,updatedAt\",\n ]\n) or []\n\npayload = {\n \"generated_at\": datetime.now(timezone.utc).isoformat(),\n \"repository\": REPO,\n \"lookback_window\": f\"{LOOKBACK_HOURS}h\",\n \"failed_run_ids\": [run.get(\"run_id\") for run in failed_runs if run.get(\"run_id\")],\n \"failures\": failure_details,\n \"existing_tracking_issues\": existing_tracking_issues,\n}\n\nwith open(OUT, \"w\", encoding=\"utf-8\") as f:\n json.dump(payload, f, indent=2)\n f.write(\"\\n\")\n\nprint(f\"Wrote deterministic prefetch payload to {OUT}\")\nprint(f\"Failed runs in payload: {len(payload['failed_run_ids'])}\")\nprint(f\"Existing tracking issues in payload: {len(existing_tracking_issues)}\")\nPY"
# Cache configuration from frontmatter processed below
- name: Failure investigator prefetch
diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml
index 3bd3507d4c3..0e5ca299442 100644
--- a/.github/workflows/ci-doctor.lock.yml
+++ b/.github/workflows/ci-doctor.lock.yml
@@ -587,7 +587,7 @@ jobs:
REPO: ${{ github.repository }}
if: github.event_name == 'pull_request'
name: Fetch PR check run status
- run: "set -e\nPR_DIR=\"/tmp/gh-aw/agent/ci-doctor/pr\"\nmkdir -p \"$PR_DIR\"\n\necho \"=== CI Doctor: Fetching check runs for PR #$PR_NUMBER (SHA: $HEAD_SHA) ===\"\n\n# Fetch all check runs for the PR head commit (paginated to handle >30 jobs)\ngh api --paginate \"repos/$REPO/commits/$HEAD_SHA/check-runs\" \\\n --jq '.check_runs[] | {id:.id, name:.name, status:.status, conclusion:.conclusion, html_url:.html_url}' \\\n | jq -s '.' \\\n > \"$PR_DIR/check-runs.json\"\n\nTOTAL=$(jq 'length' \"$PR_DIR/check-runs.json\")\nFAILED=$(jq '[.[] | select(.conclusion == \"failure\" or .conclusion == \"cancelled\" or .conclusion == \"timed_out\")] | length' \"$PR_DIR/check-runs.json\")\necho \"Found $TOTAL check run(s), $FAILED failing\"\n\n# Isolate the failing check runs\njq '[.[] | select(.conclusion == \"failure\" or .conclusion == \"cancelled\" or .conclusion == \"timed_out\")]' \\\n \"$PR_DIR/check-runs.json\" > \"$PR_DIR/failed-checks.json\"\n\n# Write a human-readable summary\nSUMMARY_FILE=\"$PR_DIR/summary.txt\"\n{\n echo \"=== CI Doctor PR Pre-Analysis ===\"\n echo \"PR: #$PR_NUMBER\"\n echo \"HEAD SHA: $HEAD_SHA\"\n echo \"Total check runs: $TOTAL\"\n echo \"Failing check runs: $FAILED\"\n echo \"\"\n echo \"All checks ($PR_DIR/check-runs.json):\"\n jq -r '.[] | \" \\(.conclusion // .status): \\(.name)\"' \"$PR_DIR/check-runs.json\"\n echo \"\"\n if [ \"$FAILED\" -gt 0 ]; then\n echo \"Failing checks ($PR_DIR/failed-checks.json):\"\n jq -r '.[] | \" - \\(.name) [\\(.conclusion)]: \\(.html_url)\"' \"$PR_DIR/failed-checks.json\"\n fi\n} | tee \"$SUMMARY_FILE\"\n\necho \"\"\necho \"ā
PR pre-analysis complete. Agent should start with $SUMMARY_FILE\"\n"
+ run: "set -e\nPR_DIR=\"/tmp/gh-aw/agent/ci-doctor/pr\"\nmkdir -p \"$PR_DIR\"\n\necho \"=== CI Doctor: Fetching check runs for PR #$PR_NUMBER (SHA: $HEAD_SHA) ===\"\n\n# Fetch all check runs for the PR head commit (paginated to handle >30 jobs)\ngh api --paginate \"repos/$REPO/commits/$HEAD_SHA/check-runs\" \\\n --jq '.check_runs[] | {id:.id, name:.name, status:.status, conclusion:.conclusion, html_url:.html_url}' \\\n | jq -s '.' \\\n > \"$PR_DIR/check-runs.json\"\n\nTOTAL=$(jq 'length' \"$PR_DIR/check-runs.json\")\nFAILED=$(jq '[.[] | select(.conclusion == \"failure\" or .conclusion == \"cancelled\" or .conclusion == \"timed_out\")] | length' \"$PR_DIR/check-runs.json\")\necho \"Found $TOTAL check run(s), $FAILED failing\"\n\n# Isolate the failing check runs\njq '[.[] | select(.conclusion == \"failure\" or .conclusion == \"cancelled\" or .conclusion == \"timed_out\")]' \\\n \"$PR_DIR/check-runs.json\" > \"$PR_DIR/failed-checks.json\"\n\n# Write a human-readable summary\nSUMMARY_FILE=\"$PR_DIR/summary.txt\"\n{\n echo \"=== CI Doctor PR Pre-Analysis ===\"\n echo \"PR: #$PR_NUMBER\"\n echo \"HEAD SHA: $HEAD_SHA\"\n echo \"Total check runs: $TOTAL\"\n echo \"Failing check runs: $FAILED\"\n echo \"\"\n echo \"All checks ($PR_DIR/check-runs.json):\"\n jq -r '.[] | \" \\(.conclusion // .status): \\(.name)\"' \"$PR_DIR/check-runs.json\"\n echo \"\"\n if [ \"$FAILED\" -gt 0 ]; then\n echo \"Failing checks ($PR_DIR/failed-checks.json):\"\n jq -r '.[] | \" - \\(.name) [\\(.conclusion)]: \\(.html_url)\"' \"$PR_DIR/failed-checks.json\"\n fi\n} | tee \"$SUMMARY_FILE\"\n\necho \"\"\necho \"ā
PR pre-analysis complete. Agent should start with $SUMMARY_FILE\""
# Cache memory file share configuration from frontmatter processed below
- name: Create cache-memory directory
diff --git a/.github/workflows/cli-consistency-checker.lock.yml b/.github/workflows/cli-consistency-checker.lock.yml
index 930d93b1c18..08666ca4fd4 100644
--- a/.github/workflows/cli-consistency-checker.lock.yml
+++ b/.github/workflows/cli-consistency-checker.lock.yml
@@ -530,7 +530,7 @@ jobs:
GH_AW_SKILL_DIR: ".github/skills"
run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh"
- name: Build CLI and pre-collect help output
- run: "set -euo pipefail\ncd /home/runner/work/gh-aw/gh-aw\nmake build\n\noutput_dir=\"/tmp/gh-aw/agent/help-output\"\nmkdir -p \"${output_dir}\"\nextract_commands='\n /^[[:space:]]+[[:alnum:]_-]+([[:space:]]|$)/ {\n cmd=$1\n gsub(/:$/, \"\", cmd)\n if (cmd != \"\" && cmd != \"Commands\") print cmd\n }\n'\n\n./gh-aw --help > \"${output_dir}/main.txt\"\nmapfile -t top_commands < <(awk \"${extract_commands}\" \"${output_dir}/main.txt\" | sort -u)\n\nfor cmd in \"${top_commands[@]}\"; do\n if ! ./gh-aw \"$cmd\" --help > \"${output_dir}/${cmd}.txt\" 2>&1; then\n echo \"warning: failed to collect help for '${cmd}'\" >&2\n continue\n fi\n mapfile -t subcommands < <(awk \"${extract_commands}\" \"${output_dir}/${cmd}.txt\" | sort -u)\n for sub in \"${subcommands[@]}\"; do\n if ! ./gh-aw \"$cmd\" \"$sub\" --help > \"${output_dir}/${cmd}-${sub}.txt\" 2>&1; then\n echo \"warning: failed to collect help for '${cmd} ${sub}'\" >&2\n fi\n done\ndone\n\nshopt -s nullglob\nhelp_files=(\"${output_dir}\"/*.txt)\nif [ ${#help_files[@]} -eq 0 ]; then\n echo \"No help output files were generated\" >&2\n exit 1\nfi\ncat \"${help_files[@]}\" > /tmp/gh-aw/agent/all-help.txt\nwc -l /tmp/gh-aw/agent/all-help.txt | awk '{print \"Pre-collected help lines:\", $1}'\n"
+ run: "set -euo pipefail\ncd /home/runner/work/gh-aw/gh-aw\nmake build\n\noutput_dir=\"/tmp/gh-aw/agent/help-output\"\nmkdir -p \"${output_dir}\"\nextract_commands='\n /^[[:space:]]+[[:alnum:]_-]+([[:space:]]|$)/ {\n cmd=$1\n gsub(/:$/, \"\", cmd)\n if (cmd != \"\" && cmd != \"Commands\") print cmd\n }\n'\n\n./gh-aw --help > \"${output_dir}/main.txt\"\nmapfile -t top_commands < <(awk \"${extract_commands}\" \"${output_dir}/main.txt\" | sort -u)\n\nfor cmd in \"${top_commands[@]}\"; do\n if ! ./gh-aw \"$cmd\" --help > \"${output_dir}/${cmd}.txt\" 2>&1; then\n echo \"warning: failed to collect help for '${cmd}'\" >&2\n continue\n fi\n mapfile -t subcommands < <(awk \"${extract_commands}\" \"${output_dir}/${cmd}.txt\" | sort -u)\n for sub in \"${subcommands[@]}\"; do\n if ! ./gh-aw \"$cmd\" \"$sub\" --help > \"${output_dir}/${cmd}-${sub}.txt\" 2>&1; then\n echo \"warning: failed to collect help for '${cmd} ${sub}'\" >&2\n fi\n done\ndone\n\nshopt -s nullglob\nhelp_files=(\"${output_dir}\"/*.txt)\nif [ ${#help_files[@]} -eq 0 ]; then\n echo \"No help output files were generated\" >&2\n exit 1\nfi\ncat \"${help_files[@]}\" > /tmp/gh-aw/agent/all-help.txt\nwc -l /tmp/gh-aw/agent/all-help.txt | awk '{print \"Pre-collected help lines:\", $1}'"
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4
diff --git a/.github/workflows/code-simplifier.lock.yml b/.github/workflows/code-simplifier.lock.yml
index c79d3d5242a..ed42078ff2c 100644
--- a/.github/workflows/code-simplifier.lock.yml
+++ b/.github/workflows/code-simplifier.lock.yml
@@ -501,7 +501,7 @@ jobs:
EXPR_GITHUB_REPOSITORY: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Prepare workflow history summary (deterministic)
- run: "set -euo pipefail\n: \"${GH_TOKEN:?GH_TOKEN is required for gh CLI queries}\"\nmkdir -p /tmp/gh-aw/agent/code-simplifier\n\ngh api \"repos/$EXPR_GITHUB_REPOSITORY/actions/workflows/code-simplifier.lock.yml/runs?per_page=30\" \\\n > /tmp/gh-aw/agent/code-simplifier/workflow-runs.json || echo '{\"workflow_runs\":[]}' > /tmp/gh-aw/agent/code-simplifier/workflow-runs.json\n\njq '\n .workflow_runs // []\n | {\n sample_size: length,\n success_count: (map(select(.conclusion == \"success\")) | length),\n failure_count: (map(select(.conclusion == \"failure\")) | length),\n cancelled_count: (map(select(.conclusion == \"cancelled\")) | length),\n recent_failures: (map(select(.conclusion == \"failure\")) | map({id,created_at,html_url}) | .[0:5]),\n deterministic_candidates: [\n {\n name: \"recent PR and commit discovery\",\n move_to: \"steps\",\n reason: \"This data is fetched every run and can be pre-computed once in deterministic shell steps.\"\n },\n {\n name: \"changed-file filtering\",\n move_to: \"steps\",\n reason: \"Extension and test/generated-file filtering is deterministic and should not consume AI tokens.\"\n },\n {\n name: \"workflow run history aggregation\",\n move_to: \"steps\",\n reason: \"Historical run metadata can be summarized via jq before entering agent context.\"\n }\n ]\n }\n' /tmp/gh-aw/agent/code-simplifier/workflow-runs.json > /tmp/gh-aw/agent/code-simplifier/history-summary.json\n"
+ run: "set -euo pipefail\n: \"${GH_TOKEN:?GH_TOKEN is required for gh CLI queries}\"\nmkdir -p /tmp/gh-aw/agent/code-simplifier\n\ngh api \"repos/$EXPR_GITHUB_REPOSITORY/actions/workflows/code-simplifier.lock.yml/runs?per_page=30\" \\\n > /tmp/gh-aw/agent/code-simplifier/workflow-runs.json || echo '{\"workflow_runs\":[]}' > /tmp/gh-aw/agent/code-simplifier/workflow-runs.json\n\njq '\n .workflow_runs // []\n | {\n sample_size: length,\n success_count: (map(select(.conclusion == \"success\")) | length),\n failure_count: (map(select(.conclusion == \"failure\")) | length),\n cancelled_count: (map(select(.conclusion == \"cancelled\")) | length),\n recent_failures: (map(select(.conclusion == \"failure\")) | map({id,created_at,html_url}) | .[0:5]),\n deterministic_candidates: [\n {\n name: \"recent PR and commit discovery\",\n move_to: \"steps\",\n reason: \"This data is fetched every run and can be pre-computed once in deterministic shell steps.\"\n },\n {\n name: \"changed-file filtering\",\n move_to: \"steps\",\n reason: \"Extension and test/generated-file filtering is deterministic and should not consume AI tokens.\"\n },\n {\n name: \"workflow run history aggregation\",\n move_to: \"steps\",\n reason: \"Historical run metadata can be summarized via jq before entering agent context.\"\n }\n ]\n }\n' /tmp/gh-aw/agent/code-simplifier/workflow-runs.json > /tmp/gh-aw/agent/code-simplifier/history-summary.json"
- name: Configure Git credentials
env:
diff --git a/.github/workflows/contribution-check.lock.yml b/.github/workflows/contribution-check.lock.yml
index 0112e0d7c6c..094049c3de1 100644
--- a/.github/workflows/contribution-check.lock.yml
+++ b/.github/workflows/contribution-check.lock.yml
@@ -497,7 +497,7 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/start_difc_proxy.sh"
- name: Fetch and filter PRs
- run: |
+ run: |-
# Fetch open PRs from the target repository opened in the last 24 hours
SINCE=$(date -d '24 hours ago' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
|| date -v-24H '+%Y-%m-%dT%H:%M:%SZ')
diff --git a/.github/workflows/copilot-centralization-drilldown.lock.yml b/.github/workflows/copilot-centralization-drilldown.lock.yml
index 4cb96926c59..910370bd650 100644
--- a/.github/workflows/copilot-centralization-drilldown.lock.yml
+++ b/.github/workflows/copilot-centralization-drilldown.lock.yml
@@ -460,7 +460,7 @@ jobs:
SAMPLE_PROMPT: ${{ inputs.sample_prompt }}
TARGET_KIND: ${{ inputs.target_kind }}
name: Normalize candidate input
- run: "set -euo pipefail\nGH_AW_SAFE_OUTPUTS=\"${GH_AW_SAFE_OUTPUTS:-${RUNNER_TEMP:-/tmp}/gh-aw/safeoutputs/outputs.jsonl}\"\nmkdir -p /tmp/gh-aw/data\n\nif [ -n \"${CANDIDATE_JSON}\" ]; then\n printf '%s' \"$CANDIDATE_JSON\" | jq '.' > /tmp/gh-aw/data/candidate-raw.json\nelse\n jq -n \\\n --arg title \"$CANDIDATE_TITLE\" \\\n --arg recommendation_kind \"$RECOMMENDATION_KIND\" \\\n --arg sample_prompt \"$SAMPLE_PROMPT\" \\\n --arg evidence_summary \"$EVIDENCE_SUMMARY\" \\\n '{\n title: $title,\n recommendation_kind: $recommendation_kind,\n sample_prompt: (if $sample_prompt == \"\" then null else $sample_prompt end),\n evidence_summary: (if $evidence_summary == \"\" then null else $evidence_summary end)\n }' > /tmp/gh-aw/data/candidate-raw.json\nfi\n\ncandidate_title=\"$(jq -r '.title // empty' /tmp/gh-aw/data/candidate-raw.json)\"\nsample_prompt=\"$(jq -r '.sample_prompt // empty' /tmp/gh-aw/data/candidate-raw.json)\"\nevidence_summary=\"$(jq -r '.evidence_summary // empty' /tmp/gh-aw/data/candidate-raw.json)\"\n\nif [ -z \"$candidate_title\" ]; then\n printf '%s\\n' '{\"type\":\"noop\",\"message\":\"No candidate title was provided for drilldown.\"}' >> \"$GH_AW_SAFE_OUTPUTS\"\n exit 0\nfi\n\nslug=\"$(printf '%s' \"$candidate_title\" \\\n | tr '[:upper:]' '[:lower:]' \\\n | sed 's/[^a-z0-9]/-/g' \\\n | sed 's/-\\{2,\\}/-/g' \\\n | sed 's/^-//' \\\n | sed 's/-$//' \\\n | cut -c1-48)\"\n\nif [ -z \"$slug\" ]; then\n slug=\"candidate\"\nfi\n\nresolved_target_kind=\"$TARGET_KIND\"\nif [ \"$resolved_target_kind\" = \"\" ] || [ \"$resolved_target_kind\" = \"auto\" ]; then\n case \"$RECOMMENDATION_KIND\" in\n continuous_workflow)\n resolved_target_kind=\"workflow\"\n ;;\n shared_prompt_or_chatops)\n resolved_target_kind=\"shared_prompt\"\n ;;\n keep_ad_hoc_but_standardize)\n resolved_target_kind=\"playbook\"\n ;;\n *)\n resolved_target_kind=\"workflow\"\n ;;\n esac\nfi\n\nsample_len=${#sample_prompt}\nevidence_len=${#evidence_summary}\ncandidate_strength=\"weak\"\nif [ \"$sample_len\" -ge 24 ] || [ \"$evidence_len\" -ge 40 ] || [ -n \"$CANDIDATE_JSON\" ]; then\n candidate_strength=\"strong\"\nfi\n\njq -n \\\n --arg title \"$candidate_title\" \\\n --arg slug \"$slug\" \\\n --arg recommendation_kind \"$RECOMMENDATION_KIND\" \\\n --arg resolved_target_kind \"$resolved_target_kind\" \\\n --arg sample_prompt \"$sample_prompt\" \\\n --arg evidence_summary \"$evidence_summary\" \\\n --arg candidate_strength \"$candidate_strength\" \\\n --slurpfile raw /tmp/gh-aw/data/candidate-raw.json '\n {\n title: $title,\n slug: $slug,\n recommendation_kind: $recommendation_kind,\n resolved_target_kind: $resolved_target_kind,\n candidate_strength: $candidate_strength,\n sample_prompt: (if $sample_prompt == \"\" then null else $sample_prompt end),\n evidence_summary: (if $evidence_summary == \"\" then null else $evidence_summary end),\n raw_candidate: $raw[0]\n }\n ' > /tmp/gh-aw/data/candidate.json\n\njq -n \\\n --arg resolved_target_kind \"$resolved_target_kind\" \\\n --arg slug \"$slug\" '\n {\n target_path: (\n if $resolved_target_kind == \"workflow\" then \".github/workflows/\" + $slug + \".md\"\n elif $resolved_target_kind == \"shared_prompt\" then \".github/workflows/shared/\" + $slug + \".md\"\n else \".github/workflows/shared/\" + $slug + \"-playbook.md\"\n end\n ),\n trigger_hint: (\n if $resolved_target_kind == \"workflow\" then \"Prefer workflow_dispatch first; choose schedule or event triggers only when repeated evidence justifies automation.\"\n elif $resolved_target_kind == \"shared_prompt\" then \"Prefer a reusable prompt or shared workflow component before full automation.\"\n else \"Keep this human-in-the-loop and produce a reusable playbook or prompt template.\"\n end\n ),\n implementation_bias: \"Prefer the smallest durable artifact that reduces repeated prompting without widening scope.\",\n report_style: \"Use issue sections with visible summary and one fenced draft block.\"\n }\n ' > /tmp/gh-aw/data/derived-plan.json\n"
+ run: "set -euo pipefail\nGH_AW_SAFE_OUTPUTS=\"${GH_AW_SAFE_OUTPUTS:-${RUNNER_TEMP:-/tmp}/gh-aw/safeoutputs/outputs.jsonl}\"\nmkdir -p /tmp/gh-aw/data\n\nif [ -n \"${CANDIDATE_JSON}\" ]; then\n printf '%s' \"$CANDIDATE_JSON\" | jq '.' > /tmp/gh-aw/data/candidate-raw.json\nelse\n jq -n \\\n --arg title \"$CANDIDATE_TITLE\" \\\n --arg recommendation_kind \"$RECOMMENDATION_KIND\" \\\n --arg sample_prompt \"$SAMPLE_PROMPT\" \\\n --arg evidence_summary \"$EVIDENCE_SUMMARY\" \\\n '{\n title: $title,\n recommendation_kind: $recommendation_kind,\n sample_prompt: (if $sample_prompt == \"\" then null else $sample_prompt end),\n evidence_summary: (if $evidence_summary == \"\" then null else $evidence_summary end)\n }' > /tmp/gh-aw/data/candidate-raw.json\nfi\n\ncandidate_title=\"$(jq -r '.title // empty' /tmp/gh-aw/data/candidate-raw.json)\"\nsample_prompt=\"$(jq -r '.sample_prompt // empty' /tmp/gh-aw/data/candidate-raw.json)\"\nevidence_summary=\"$(jq -r '.evidence_summary // empty' /tmp/gh-aw/data/candidate-raw.json)\"\n\nif [ -z \"$candidate_title\" ]; then\n printf '%s\\n' '{\"type\":\"noop\",\"message\":\"No candidate title was provided for drilldown.\"}' >> \"$GH_AW_SAFE_OUTPUTS\"\n exit 0\nfi\n\nslug=\"$(printf '%s' \"$candidate_title\" \\\n | tr '[:upper:]' '[:lower:]' \\\n | sed 's/[^a-z0-9]/-/g' \\\n | sed 's/-\\{2,\\}/-/g' \\\n | sed 's/^-//' \\\n | sed 's/-$//' \\\n | cut -c1-48)\"\n\nif [ -z \"$slug\" ]; then\n slug=\"candidate\"\nfi\n\nresolved_target_kind=\"$TARGET_KIND\"\nif [ \"$resolved_target_kind\" = \"\" ] || [ \"$resolved_target_kind\" = \"auto\" ]; then\n case \"$RECOMMENDATION_KIND\" in\n continuous_workflow)\n resolved_target_kind=\"workflow\"\n ;;\n shared_prompt_or_chatops)\n resolved_target_kind=\"shared_prompt\"\n ;;\n keep_ad_hoc_but_standardize)\n resolved_target_kind=\"playbook\"\n ;;\n *)\n resolved_target_kind=\"workflow\"\n ;;\n esac\nfi\n\nsample_len=${#sample_prompt}\nevidence_len=${#evidence_summary}\ncandidate_strength=\"weak\"\nif [ \"$sample_len\" -ge 24 ] || [ \"$evidence_len\" -ge 40 ] || [ -n \"$CANDIDATE_JSON\" ]; then\n candidate_strength=\"strong\"\nfi\n\njq -n \\\n --arg title \"$candidate_title\" \\\n --arg slug \"$slug\" \\\n --arg recommendation_kind \"$RECOMMENDATION_KIND\" \\\n --arg resolved_target_kind \"$resolved_target_kind\" \\\n --arg sample_prompt \"$sample_prompt\" \\\n --arg evidence_summary \"$evidence_summary\" \\\n --arg candidate_strength \"$candidate_strength\" \\\n --slurpfile raw /tmp/gh-aw/data/candidate-raw.json '\n {\n title: $title,\n slug: $slug,\n recommendation_kind: $recommendation_kind,\n resolved_target_kind: $resolved_target_kind,\n candidate_strength: $candidate_strength,\n sample_prompt: (if $sample_prompt == \"\" then null else $sample_prompt end),\n evidence_summary: (if $evidence_summary == \"\" then null else $evidence_summary end),\n raw_candidate: $raw[0]\n }\n ' > /tmp/gh-aw/data/candidate.json\n\njq -n \\\n --arg resolved_target_kind \"$resolved_target_kind\" \\\n --arg slug \"$slug\" '\n {\n target_path: (\n if $resolved_target_kind == \"workflow\" then \".github/workflows/\" + $slug + \".md\"\n elif $resolved_target_kind == \"shared_prompt\" then \".github/workflows/shared/\" + $slug + \".md\"\n else \".github/workflows/shared/\" + $slug + \"-playbook.md\"\n end\n ),\n trigger_hint: (\n if $resolved_target_kind == \"workflow\" then \"Prefer workflow_dispatch first; choose schedule or event triggers only when repeated evidence justifies automation.\"\n elif $resolved_target_kind == \"shared_prompt\" then \"Prefer a reusable prompt or shared workflow component before full automation.\"\n else \"Keep this human-in-the-loop and produce a reusable playbook or prompt template.\"\n end\n ),\n implementation_bias: \"Prefer the smallest durable artifact that reduces repeated prompting without widening scope.\",\n report_style: \"Use issue sections with visible summary and one fenced draft block.\"\n }\n ' > /tmp/gh-aw/data/derived-plan.json"
- name: Configure Git credentials
env:
diff --git a/.github/workflows/copilot-pr-nlp-analysis.lock.yml b/.github/workflows/copilot-pr-nlp-analysis.lock.yml
index 67ff903d5b0..947b21d9824 100644
--- a/.github/workflows/copilot-pr-nlp-analysis.lock.yml
+++ b/.github/workflows/copilot-pr-nlp-analysis.lock.yml
@@ -537,7 +537,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Fetch PR comments for detailed analysis
- run: "# Create comments directory\nmkdir -p /tmp/gh-aw/agent/pr-comments\n\n# Fetch detailed comments for each PR from the pre-fetched data\nPR_COUNT=$(jq 'length' /tmp/gh-aw/agent/pr-data/copilot-prs.json)\necho \"Fetching comments for $PR_COUNT PRs...\"\n\njq -r '.[].number' /tmp/gh-aw/agent/pr-data/copilot-prs.json | while read -r PR_NUM; do\n echo \"Fetching comments for PR #${PR_NUM}\"\n gh pr view \"${PR_NUM}\" \\\n --json comments,reviews,reviewComments \\\n > \"/tmp/gh-aw/agent/pr-comments/pr-${PR_NUM}.json\" 2>/dev/null || echo \"{}\" > \"/tmp/gh-aw/agent/pr-comments/pr-${PR_NUM}.json\"\n sleep 0.5 # Rate limiting\ndone\n\necho \"Comment data saved to /tmp/gh-aw/agent/pr-comments/\"\n"
+ run: "# Create comments directory\nmkdir -p /tmp/gh-aw/agent/pr-comments\n\n# Fetch detailed comments for each PR from the pre-fetched data\nPR_COUNT=$(jq 'length' /tmp/gh-aw/agent/pr-data/copilot-prs.json)\necho \"Fetching comments for $PR_COUNT PRs...\"\n\njq -r '.[].number' /tmp/gh-aw/agent/pr-data/copilot-prs.json | while read -r PR_NUM; do\n echo \"Fetching comments for PR #${PR_NUM}\"\n gh pr view \"${PR_NUM}\" \\\n --json comments,reviews,reviewComments \\\n > \"/tmp/gh-aw/agent/pr-comments/pr-${PR_NUM}.json\" 2>/dev/null || echo \"{}\" > \"/tmp/gh-aw/agent/pr-comments/pr-${PR_NUM}.json\"\n sleep 0.5 # Rate limiting\ndone\n\necho \"Comment data saved to /tmp/gh-aw/agent/pr-comments/\""
# Cache memory file share configuration from frontmatter processed below
- name: Create cache-memory directory
diff --git a/.github/workflows/daily-ambient-context-optimizer.lock.yml b/.github/workflows/daily-ambient-context-optimizer.lock.yml
index a32ef0424a2..70f2f6fa5ca 100644
--- a/.github/workflows/daily-ambient-context-optimizer.lock.yml
+++ b/.github/workflows/daily-ambient-context-optimizer.lock.yml
@@ -521,7 +521,7 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- script: "const fs = require('fs');\n\nconst { owner, repo } = context.repo;\nconst OPTIMIZER_PATTERNS = [\n 'copilot/ambient-context',\n 'copilot/daily-ambient-context',\n 'ambient-context-optim',\n];\n\nconst cutoff7d = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n\nconst allPrs = await github.paginate(github.rest.pulls.list, {\n owner, repo, state: 'all', per_page: 100,\n});\n\nconst optPrs7d = allPrs.filter((pr) => {\n const branch = pr.head?.ref || '';\n return (\n OPTIMIZER_PATTERNS.some((p) => branch.includes(p)) &&\n new Date(pr.created_at) >= cutoff7d\n );\n});\n\nconst merged7d = optPrs7d.filter((pr) => pr.merged_at);\nconst closed7d = optPrs7d.filter((pr) => !pr.merged_at && pr.closed_at);\nconst totalSettled = merged7d.length + closed7d.length;\n\n// close_rate is null when sample < 3 to avoid misleading zero reads\nconst closeRate = totalSettled >= 3 ? closed7d.length / totalSettled : null;\nconst autoPause = closeRate !== null && closeRate >= 0.30;\n\nconst rateData = {\n merged_7d: merged7d.length,\n closed_7d: closed7d.length,\n total_settled_7d: totalSettled,\n close_rate: closeRate !== null ? Math.round(closeRate * 1000) / 1000 : null,\n auto_pause: autoPause,\n note: 'close_rate is null when total_settled_7d < 3 (insufficient data for auto-pause)',\n};\n\nfs.writeFileSync('/tmp/gh-aw/ambient-context/pr-close-rate.json', JSON.stringify(rateData, null, 2));\n\nconst rateStr = closeRate !== null ? `${Math.round(closeRate * 100)}%` : 'N/A (< 3 settled PRs)';\ncore.info(`PR close-rate (7d): ${rateStr} (${closed7d.length} closed, ${merged7d.length} merged)${autoPause ? ' ā AUTO-PAUSE ACTIVE' : ''}`);\n"
+ script: "const fs = require('fs');\n\nconst { owner, repo } = context.repo;\nconst OPTIMIZER_PATTERNS = [\n 'copilot/ambient-context',\n 'copilot/daily-ambient-context',\n 'ambient-context-optim',\n];\n\nconst cutoff7d = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);\n\nconst allPrs = await github.paginate(github.rest.pulls.list, {\n owner, repo, state: 'all', per_page: 100,\n});\n\nconst optPrs7d = allPrs.filter((pr) => {\n const branch = pr.head?.ref || '';\n return (\n OPTIMIZER_PATTERNS.some((p) => branch.includes(p)) &&\n new Date(pr.created_at) >= cutoff7d\n );\n});\n\nconst merged7d = optPrs7d.filter((pr) => pr.merged_at);\nconst closed7d = optPrs7d.filter((pr) => !pr.merged_at && pr.closed_at);\nconst totalSettled = merged7d.length + closed7d.length;\n\n// close_rate is null when sample < 3 to avoid misleading zero reads\nconst closeRate = totalSettled >= 3 ? closed7d.length / totalSettled : null;\nconst autoPause = closeRate !== null && closeRate >= 0.30;\n\nconst rateData = {\n merged_7d: merged7d.length,\n closed_7d: closed7d.length,\n total_settled_7d: totalSettled,\n close_rate: closeRate !== null ? Math.round(closeRate * 1000) / 1000 : null,\n auto_pause: autoPause,\n note: 'close_rate is null when total_settled_7d < 3 (insufficient data for auto-pause)',\n};\n\nfs.writeFileSync('/tmp/gh-aw/ambient-context/pr-close-rate.json', JSON.stringify(rateData, null, 2));\n\nconst rateStr = closeRate !== null ? `${Math.round(closeRate * 100)}%` : 'N/A (< 3 settled PRs)';\ncore.info(`PR close-rate (7d): ${rateStr} (${closed7d.length} closed, ${merged7d.length} merged)${autoPause ? ' ā AUTO-PAUSE ACTIVE' : ''}`);"
- name: Configure Git credentials
env:
diff --git a/.github/workflows/daily-community-attribution.lock.yml b/.github/workflows/daily-community-attribution.lock.yml
index a840bc33df7..a9a473a9f14 100644
--- a/.github/workflows/daily-community-attribution.lock.yml
+++ b/.github/workflows/daily-community-attribution.lock.yml
@@ -546,7 +546,7 @@ jobs:
name: Compute deterministic attributions (Tier 0, 1, 2)
run: "# Tier 0: COMPLETED issues (direct contributions, no PR needed)\njq '[.[] | select(.stateReason == \"COMPLETED\") |\n . + {tier: 0, attribution_type: \"direct issue\", closing_prs: []}]' \\\n /tmp/gh-aw/agent/community-data/community_issues.json \\\n > /tmp/gh-aw/agent/community-data/tier0_attributed.json\n\nT0=$(jq length /tmp/gh-aw/agent/community-data/tier0_attributed.json)\necho \"Tier 0 (direct issue ā COMPLETED): $T0\"\n\n# Tier 1: native GitHub close references (exclude Tier 0 issues)\njq --slurpfile issues /tmp/gh-aw/agent/community-data/community_issues.json \\\n --slurpfile t0 /tmp/gh-aw/agent/community-data/tier0_attributed.json '\n ($t0[0] | map(.number) | map(tostring)) as $t0_keys |\n ($issues[0] | map(.number | tostring)) as $issue_keys |\n to_entries |\n map(select(\n .key as $k |\n ($issue_keys | index($k) != null) and\n ($t0_keys | index($k) == null)\n )) |\n map(.key as $k | .value as $prs |\n ($issues[0] | map(select(.number | tostring == $k))[0]) +\n {tier: 1, attribution_type: \"resolved by PR\", closing_prs: $prs}\n )\n' /tmp/gh-aw/agent/community-data/closing_refs_by_issue.json \\\n > /tmp/gh-aw/agent/community-data/tier1_attributed.json 2>/dev/null \\\n || echo \"[]\" > /tmp/gh-aw/agent/community-data/tier1_attributed.json\n\nT1=$(jq length /tmp/gh-aw/agent/community-data/tier1_attributed.json)\necho \"Tier 1 (native close refs): $T1\"\n\n# Tier 2: PR body keyword matching (exclude Tier 0 and Tier 1 issues)\nKW_ISSUES=$(jq -r '.[].body // \"\"' /tmp/gh-aw/agent/community-data/pull_requests.json \\\n | grep -oP '(?i)(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s*(?:github/gh-aw#|#)\\K[0-9]+' 2>/dev/null \\\n | sort -u | jq -R 'tonumber' | jq -s 'sort | unique' 2>/dev/null \\\n || echo \"[]\")\n\njq --argjson kw \"$KW_ISSUES\" \\\n --slurpfile t0 /tmp/gh-aw/agent/community-data/tier0_attributed.json \\\n --slurpfile t1 /tmp/gh-aw/agent/community-data/tier1_attributed.json '\n ($t0[0] | map(.number)) as $t0_nums |\n ($t1[0] | map(.number)) as $t1_nums |\n [.[] |\n select(\n .number as $n |\n ($kw | index($n) != null) and\n ($t0_nums | index($n) == null) and\n ($t1_nums | index($n) == null)\n ) |\n . + {tier: 2, attribution_type: \"resolved by PR\", closing_prs: []}\n ]\n' /tmp/gh-aw/agent/community-data/community_issues.json \\\n > /tmp/gh-aw/agent/community-data/tier2_attributed.json 2>/dev/null \\\n || echo \"[]\" > /tmp/gh-aw/agent/community-data/tier2_attributed.json\n\nT2=$(jq length /tmp/gh-aw/agent/community-data/tier2_attributed.json)\necho \"Tier 2 (PR body keywords): $T2\"\n\n# Combine Tier 0 + 1 + 2 into pre_attributed.json\njq -n \\\n --slurpfile t0 /tmp/gh-aw/agent/community-data/tier0_attributed.json \\\n --slurpfile t1 /tmp/gh-aw/agent/community-data/tier1_attributed.json \\\n --slurpfile t2 /tmp/gh-aw/agent/community-data/tier2_attributed.json \\\n '$t0[0] + $t1[0] + $t2[0]' \\\n > /tmp/gh-aw/agent/community-data/pre_attributed.json\n\nTOTAL=$(jq length /tmp/gh-aw/agent/community-data/pre_attributed.json)\necho \"\"\necho \"Pre-attributed: $TOTAL issues (Tier 0: $T0, Tier 1: $T1, Tier 2: $T2)\"\n\n# Compute Tier 3+ candidates (closed in window, not yet pre-attributed)\njq --slurpfile pre /tmp/gh-aw/agent/community-data/pre_attributed.json '\n ($pre[0] | map(.number)) as $attributed |\n [.[] | select(.number as $n | $attributed | index($n) == null)]\n' /tmp/gh-aw/agent/community-data/community_issues_closed_in_window.json \\\n > /tmp/gh-aw/agent/community-data/tier3_candidates.json 2>/dev/null \\\n || echo \"[]\" > /tmp/gh-aw/agent/community-data/tier3_candidates.json\n\nT3=$(jq length /tmp/gh-aw/agent/community-data/tier3_candidates.json)\necho \"Tier 3+ candidates (agent lookup needed): $T3\"\n\n# Cap Tier 3 to 5 per run ā prevents runaway API call loops when there are\n# many unlinked issues. Remaining candidates will be processed in future runs.\njq '.[0:5]' /tmp/gh-aw/agent/community-data/tier3_candidates.json \\\n > /tmp/gh-aw/agent/community-data/tier3_candidates_capped.json\nT3_CAPPED=$(jq length /tmp/gh-aw/agent/community-data/tier3_candidates_capped.json)\nif [ \"$T3_CAPPED\" -lt \"$T3\" ]; then\n echo \"ā Capped Tier 3 lookups: processing $T3_CAPPED of $T3 candidates this run\"\nfi\necho \"\"\necho \"Data available in /tmp/gh-aw/agent/community-data/:\"\necho \" pre_attributed.json ā Tier 0+1+2 confirmed attributions\"\necho \" tier3_candidates_capped.json ā up to 5 issues needing Tier 3 agent lookup (this run)\"\necho \" tier3_candidates.json ā full list of Tier 3+ candidates (for reference)\"\n"
- name: Format attribution data for agent
- run: "DATA_DIR=/tmp/gh-aw/agent/community-data\n\n# Group Tier 0-2 attributions by author for agent-ready consumption.\n# Produces a structured JSON the agent can read with `cat` ā no jq needed.\njq '\n group_by(.author.login) |\n sort_by(.[0].author.login | ascii_downcase) |\n map({\n author: .[0].author.login,\n count: length,\n issues: (sort_by(-.number))\n })\n' \"$DATA_DIR/pre_attributed.json\" \\\n > \"$DATA_DIR/attribution_by_author.json\" 2>/dev/null \\\n || echo \"[]\" > \"$DATA_DIR/attribution_by_author.json\"\n\nAUTHOR_COUNT=$(jq length \"$DATA_DIR/attribution_by_author.json\")\nISSUE_COUNT=$(jq length \"$DATA_DIR/pre_attributed.json\")\necho \"ā Grouped: $AUTHOR_COUNT authors, $ISSUE_COUNT issues\"\n\n# Generate the pre-formatted README community section (Tier 0-2 only).\n# The agent reads this file directly, appends Tier 3 results, and inserts\n# the result into README.md ā no bash data-processing required.\n{\n REPO_URL=\"https://github.com/${GITHUB_REPOSITORY:-github/gh-aw}\"\n SEARCH_BASE=\"${REPO_URL}/issues?q=is%3Aissue+is%3Aclosed+label%3Acommunity+author%3A\"\n echo \"## š Community Contributions\"\n echo \"\"\n echo \"Community members whose issues were resolved ā updated automatically.\"\n echo \"\"\n jq -r --arg base \"$SEARCH_BASE\" '\n .[] |\n \"[@\\(.author) (\\(.count))](\\($base + .author))\"\n ' \"$DATA_DIR/attribution_by_author.json\"\n echo \"\"\n} > \"$DATA_DIR/readme_community_section_tier012.md\"\n\necho \"ā Generated readme_community_section_tier012.md\"\necho \"\"\necho \"Data available in $DATA_DIR/:\"\necho \" attribution_by_author.json ā Tier 0-2 issues grouped by author (agent-ready)\"\necho \" readme_community_section_tier012.md ā pre-formatted README section (Tier 0-2 only)\"\n"
+ run: "DATA_DIR=/tmp/gh-aw/agent/community-data\n\n# Group Tier 0-2 attributions by author for agent-ready consumption.\n# Produces a structured JSON the agent can read with `cat` ā no jq needed.\njq '\n group_by(.author.login) |\n sort_by(.[0].author.login | ascii_downcase) |\n map({\n author: .[0].author.login,\n count: length,\n issues: (sort_by(-.number))\n })\n' \"$DATA_DIR/pre_attributed.json\" \\\n > \"$DATA_DIR/attribution_by_author.json\" 2>/dev/null \\\n || echo \"[]\" > \"$DATA_DIR/attribution_by_author.json\"\n\nAUTHOR_COUNT=$(jq length \"$DATA_DIR/attribution_by_author.json\")\nISSUE_COUNT=$(jq length \"$DATA_DIR/pre_attributed.json\")\necho \"ā Grouped: $AUTHOR_COUNT authors, $ISSUE_COUNT issues\"\n\n# Generate the pre-formatted README community section (Tier 0-2 only).\n# The agent reads this file directly, appends Tier 3 results, and inserts\n# the result into README.md ā no bash data-processing required.\n{\n REPO_URL=\"https://github.com/${GITHUB_REPOSITORY:-github/gh-aw}\"\n SEARCH_BASE=\"${REPO_URL}/issues?q=is%3Aissue+is%3Aclosed+label%3Acommunity+author%3A\"\n echo \"## š Community Contributions\"\n echo \"\"\n echo \"Community members whose issues were resolved ā updated automatically.\"\n echo \"\"\n jq -r --arg base \"$SEARCH_BASE\" '\n .[] |\n \"[@\\(.author) (\\(.count))](\\($base + .author))\"\n ' \"$DATA_DIR/attribution_by_author.json\"\n echo \"\"\n} > \"$DATA_DIR/readme_community_section_tier012.md\"\n\necho \"ā Generated readme_community_section_tier012.md\"\necho \"\"\necho \"Data available in $DATA_DIR/:\"\necho \" attribution_by_author.json ā Tier 0-2 issues grouped by author (agent-ready)\"\necho \" readme_community_section_tier012.md ā pre-formatted README section (Tier 0-2 only)\""
# Repo memory git-based storage configuration from frontmatter processed below
- name: Clone wiki-memory branch (default)
diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml
index eca14f3a017..16cc64a7a7c 100644
--- a/.github/workflows/daily-multi-device-docs-tester.lock.yml
+++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml
@@ -576,7 +576,7 @@ jobs:
- env:
EXPR_GITHUB_RUN_ID: ${{ github.run_id }}
name: Wait for server readiness
- run: "PID_FILE=\"/tmp/gh-aw/agent/docs-server-$EXPR_GITHUB_RUN_ID.pid\"\nLOG_FILE=\"/tmp/gh-aw/agent/docs-server-$EXPR_GITHUB_RUN_ID.log\"\nMAX_WAIT=135 # Maximum 135 seconds wait time\nWAITED=0\nuntil curl -sf http://localhost:4321/gh-aw/ > /dev/null 2>&1; do\n # Check if the server process has already died\n if [ -f \"$PID_FILE\" ] && ! kill -0 \"$(cat \"$PID_FILE\")\" 2>/dev/null; then\n echo \"::error::Documentation server process died before becoming ready. Server log:\"\n cat \"$LOG_FILE\"\n exit 1\n fi\n WAITED=$((WAITED + 3))\n if [ $WAITED -ge $MAX_WAIT ]; then\n echo \"::error::Documentation server did not start after ${MAX_WAIT}s. Server log:\"\n cat \"$LOG_FILE\"\n exit 1\n fi\n echo \"Waiting for server... ($WAITED/${MAX_WAIT}s)\"\n sleep 3\ndone\necho \"Server ready at http://localhost:4321/gh-aw/!\"\n"
+ run: "PID_FILE=\"/tmp/gh-aw/agent/docs-server-$EXPR_GITHUB_RUN_ID.pid\"\nLOG_FILE=\"/tmp/gh-aw/agent/docs-server-$EXPR_GITHUB_RUN_ID.log\"\nMAX_WAIT=135 # Maximum 135 seconds wait time\nWAITED=0\nuntil curl -sf http://localhost:4321/gh-aw/ > /dev/null 2>&1; do\n # Check if the server process has already died\n if [ -f \"$PID_FILE\" ] && ! kill -0 \"$(cat \"$PID_FILE\")\" 2>/dev/null; then\n echo \"::error::Documentation server process died before becoming ready. Server log:\"\n cat \"$LOG_FILE\"\n exit 1\n fi\n WAITED=$((WAITED + 3))\n if [ $WAITED -ge $MAX_WAIT ]; then\n echo \"::error::Documentation server did not start after ${MAX_WAIT}s. Server log:\"\n cat \"$LOG_FILE\"\n exit 1\n fi\n echo \"Waiting for server... ($WAITED/${MAX_WAIT}s)\"\n sleep 3\ndone\necho \"Server ready at http://localhost:4321/gh-aw/!\""
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.24@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4
diff --git a/.github/workflows/daily-security-observability.lock.yml b/.github/workflows/daily-security-observability.lock.yml
index 384569f9a99..f7b2d9430ff 100644
--- a/.github/workflows/daily-security-observability.lock.yml
+++ b/.github/workflows/daily-security-observability.lock.yml
@@ -578,7 +578,7 @@ jobs:
- env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Download integrity-filtered logs
- run: "mkdir -p /tmp/gh-aw/agent/integrity\nmkdir -p /tmp/gh-aw/cache-memory/security-observability\n\nCACHE_FILE=/tmp/gh-aw/cache-memory/security-observability/filtered-logs.snapshot.json\nRUN_FILE=/tmp/gh-aw/agent/integrity/filtered-logs.json\nFRESH_LOGS=/tmp/gh-aw/agent/integrity/filtered-logs.fresh.json\nEMPTY_DATA='{\"runs\":[],\"summary\":{\"total_runs\":0}}'\nNOW_EPOCH=$(date +%s)\nMAX_CACHE_AGE_SECONDS=$((7 * 24 * 60 * 60))\n\n# Warm start from cached 7-day snapshot when available and fresh.\nif [ -f \"$CACHE_FILE\" ] && jq -e '.runs and .updated_at' \"$CACHE_FILE\" > /dev/null 2>&1; then\n cache_updated_at=$(jq -r '.updated_at' \"$CACHE_FILE\")\n cache_updated_epoch=$(\n date -d \"$cache_updated_at\" +%s 2>/dev/null \\\n || date -j -f \"%Y-%m-%dT%H:%M:%SZ\" \"$cache_updated_at\" +%s 2>/dev/null \\\n || echo 0\n )\n cache_age_seconds=$((NOW_EPOCH - cache_updated_epoch))\n if [ \"$cache_updated_epoch\" -gt 0 ] && [ \"$cache_age_seconds\" -le \"$MAX_CACHE_AGE_SECONDS\" ]; then\n jq '{runs: (.runs // []), summary: (.summary // {\"total_runs\": 0})}' \"$CACHE_FILE\" > \"$RUN_FILE\"\n echo \"ā
Warm cache restored (${cache_age_seconds}s old)\"\n else\n echo \"ā ļø Cache snapshot is stale (${cache_age_seconds}s old); starting fresh\"\n fi\nfi\n\n# Download logs filtered to only runs with DIFC integrity-filtered events.\n# --artifacts mcp: only download the MCP gateway log artifact (sufficient for DIFC checking).\n# --timeout 8: cap execution at 8 minutes to prevent runaway downloads.\ngh aw logs --filtered-integrity --start-date -7d --json -c 200 \\\n --artifacts mcp --timeout 8 \\\n > \"$FRESH_LOGS\" || true\n\n# Validate JSON output and fall back to an empty dataset on failure\nif ! jq -e '.runs' \"$FRESH_LOGS\" > /dev/null 2>&1; then\n echo \"ā ļø No valid logs produced; continuing with empty dataset\"\n echo \"$EMPTY_DATA\" > \"$FRESH_LOGS\"\nfi\n\n# Merge warm-start and fresh runs; fresh entries override warm-cache entries with the same run_id.\nif [ -f \"$RUN_FILE\" ] && jq -e '.runs' \"$RUN_FILE\" > /dev/null 2>&1; then\n jq -s '\n {\n runs: (\n ((.[0].runs // []) + (.[1].runs // []))\n | sort_by(.run_id)\n | group_by(.run_id)\n | map(.[-1])\n ),\n summary: {\n total_runs: (\n ((.[0].runs // []) + (.[1].runs // []) | sort_by(.run_id) | group_by(.run_id) | length)\n )\n }\n }\n ' \"$RUN_FILE\" \"$FRESH_LOGS\" > \"$RUN_FILE.merged\"\n mv \"$RUN_FILE.merged\" \"$RUN_FILE\"\nelse\n mv \"$FRESH_LOGS\" \"$RUN_FILE\"\nfi\n\ncount=$(jq '.runs | length' \"$RUN_FILE\" 2>/dev/null || echo 0)\necho \"ā
Downloaded $count runs with integrity-filtered events\"\n\n# Persist updated 7-day snapshot back to cache-memory every run.\njq -n \\\n --arg updated_at \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n --slurpfile payload \"$RUN_FILE\" \\\n '{\n updated_at: $updated_at,\n runs: ($payload[0].runs // []),\n summary: {\n total_runs: (($payload[0].runs // []) | length)\n }\n }' > \"$CACHE_FILE\"\n"
+ run: "mkdir -p /tmp/gh-aw/agent/integrity\nmkdir -p /tmp/gh-aw/cache-memory/security-observability\n\nCACHE_FILE=/tmp/gh-aw/cache-memory/security-observability/filtered-logs.snapshot.json\nRUN_FILE=/tmp/gh-aw/agent/integrity/filtered-logs.json\nFRESH_LOGS=/tmp/gh-aw/agent/integrity/filtered-logs.fresh.json\nEMPTY_DATA='{\"runs\":[],\"summary\":{\"total_runs\":0}}'\nNOW_EPOCH=$(date +%s)\nMAX_CACHE_AGE_SECONDS=$((7 * 24 * 60 * 60))\n\n# Warm start from cached 7-day snapshot when available and fresh.\nif [ -f \"$CACHE_FILE\" ] && jq -e '.runs and .updated_at' \"$CACHE_FILE\" > /dev/null 2>&1; then\n cache_updated_at=$(jq -r '.updated_at' \"$CACHE_FILE\")\n cache_updated_epoch=$(\n date -d \"$cache_updated_at\" +%s 2>/dev/null \\\n || date -j -f \"%Y-%m-%dT%H:%M:%SZ\" \"$cache_updated_at\" +%s 2>/dev/null \\\n || echo 0\n )\n cache_age_seconds=$((NOW_EPOCH - cache_updated_epoch))\n if [ \"$cache_updated_epoch\" -gt 0 ] && [ \"$cache_age_seconds\" -le \"$MAX_CACHE_AGE_SECONDS\" ]; then\n jq '{runs: (.runs // []), summary: (.summary // {\"total_runs\": 0})}' \"$CACHE_FILE\" > \"$RUN_FILE\"\n echo \"ā
Warm cache restored (${cache_age_seconds}s old)\"\n else\n echo \"ā ļø Cache snapshot is stale (${cache_age_seconds}s old); starting fresh\"\n fi\nfi\n\n# Download logs filtered to only runs with DIFC integrity-filtered events.\n# --artifacts mcp: only download the MCP gateway log artifact (sufficient for DIFC checking).\n# --timeout 8: cap execution at 8 minutes to prevent runaway downloads.\ngh aw logs --filtered-integrity --start-date -7d --json -c 200 \\\n --artifacts mcp --timeout 8 \\\n > \"$FRESH_LOGS\" || true\n\n# Validate JSON output and fall back to an empty dataset on failure\nif ! jq -e '.runs' \"$FRESH_LOGS\" > /dev/null 2>&1; then\n echo \"ā ļø No valid logs produced; continuing with empty dataset\"\n echo \"$EMPTY_DATA\" > \"$FRESH_LOGS\"\nfi\n\n# Merge warm-start and fresh runs; fresh entries override warm-cache entries with the same run_id.\nif [ -f \"$RUN_FILE\" ] && jq -e '.runs' \"$RUN_FILE\" > /dev/null 2>&1; then\n jq -s '\n {\n runs: (\n ((.[0].runs // []) + (.[1].runs // []))\n | sort_by(.run_id)\n | group_by(.run_id)\n | map(.[-1])\n ),\n summary: {\n total_runs: (\n ((.[0].runs // []) + (.[1].runs // []) | sort_by(.run_id) | group_by(.run_id) | length)\n )\n }\n }\n ' \"$RUN_FILE\" \"$FRESH_LOGS\" > \"$RUN_FILE.merged\"\n mv \"$RUN_FILE.merged\" \"$RUN_FILE\"\nelse\n mv \"$FRESH_LOGS\" \"$RUN_FILE\"\nfi\n\ncount=$(jq '.runs | length' \"$RUN_FILE\" 2>/dev/null || echo 0)\necho \"ā
Downloaded $count runs with integrity-filtered events\"\n\n# Persist updated 7-day snapshot back to cache-memory every run.\njq -n \\\n --arg updated_at \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n --slurpfile payload \"$RUN_FILE\" \\\n '{\n updated_at: $updated_at,\n runs: ($payload[0].runs // []),\n summary: {\n total_runs: (($payload[0].runs // []) | length)\n }\n }' > \"$CACHE_FILE\""
# Cache memory file share configuration from frontmatter processed below
- name: Create cache-memory directory
diff --git a/.github/workflows/daily-spdd-spec-planner.lock.yml b/.github/workflows/daily-spdd-spec-planner.lock.yml
index c30b8925fa7..df68af6c566 100644
--- a/.github/workflows/daily-spdd-spec-planner.lock.yml
+++ b/.github/workflows/daily-spdd-spec-planner.lock.yml
@@ -482,7 +482,7 @@ jobs:
- env:
GH_TOKEN: ${{ github.token }}
name: Copy OpenSPDD prompts
- run: "set -euo pipefail\n# Resolve the latest OpenSPDD main ref each run via authenticated GitHub API.\n# This intentionally tracks upstream prompt updates while avoiding unauthenticated rate limits.\nOPENSPDD_REF=\"$(gh api repos/gszhangwei/open-spdd/commits/main --jq .sha)\"\ntest -n \"${OPENSPDD_REF}\" || { echo \"::error::Failed to resolve OpenSPDD main ref\"; exit 1; }\nPROMPTS_DIR=\"${GITHUB_WORKSPACE}/.github/copilot-prompts\"\nmkdir -p \"${PROMPTS_DIR}\"\nfor PROMPT in spdd-analysis spdd-reasons-canvas spdd-generate spdd-sync; do\n gh api \\\n -H \"Accept: application/vnd.github.raw\" \\\n \"repos/gszhangwei/open-spdd/contents/.cursor/commands/${PROMPT}.md?ref=${OPENSPDD_REF}\" \\\n > \"${PROMPTS_DIR}/${PROMPT}.md\"\n test -s \"${PROMPTS_DIR}/${PROMPT}.md\" || { echo \"::error::Failed to download ${PROMPT}.md\"; exit 1; }\ndone\n"
+ run: "set -euo pipefail\n# Resolve the latest OpenSPDD main ref each run via authenticated GitHub API.\n# This intentionally tracks upstream prompt updates while avoiding unauthenticated rate limits.\nOPENSPDD_REF=\"$(gh api repos/gszhangwei/open-spdd/commits/main --jq .sha)\"\ntest -n \"${OPENSPDD_REF}\" || { echo \"::error::Failed to resolve OpenSPDD main ref\"; exit 1; }\nPROMPTS_DIR=\"${GITHUB_WORKSPACE}/.github/copilot-prompts\"\nmkdir -p \"${PROMPTS_DIR}\"\nfor PROMPT in spdd-analysis spdd-reasons-canvas spdd-generate spdd-sync; do\n gh api \\\n -H \"Accept: application/vnd.github.raw\" \\\n \"repos/gszhangwei/open-spdd/contents/.cursor/commands/${PROMPT}.md?ref=${OPENSPDD_REF}\" \\\n > \"${PROMPTS_DIR}/${PROMPT}.md\"\n test -s \"${PROMPTS_DIR}/${PROMPT}.md\" || { echo \"::error::Failed to download ${PROMPT}.md\"; exit 1; }\ndone"
# Cache memory file share configuration from frontmatter processed below
- name: Create cache-memory directory
diff --git a/.github/workflows/dataflow-pr-discussion-dataset.lock.yml b/.github/workflows/dataflow-pr-discussion-dataset.lock.yml
index abc815c7377..44b80660eb5 100644
--- a/.github/workflows/dataflow-pr-discussion-dataset.lock.yml
+++ b/.github/workflows/dataflow-pr-discussion-dataset.lock.yml
@@ -738,7 +738,7 @@ jobs:
GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql
NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt
- name: Fetch merged PRs
- run: |
+ run: |-
bash "${RUNNER_TEMP}/gh-aw/actions/install_gh_cli.sh"
# Fetch up to 500 merged PRs ā title, body, metadata
diff --git a/.github/workflows/delight.lock.yml b/.github/workflows/delight.lock.yml
index 307ba303ca6..aa8848c86aa 100644
--- a/.github/workflows/delight.lock.yml
+++ b/.github/workflows/delight.lock.yml
@@ -555,7 +555,7 @@ jobs:
GH_AW_SKILL_DIR: ".github/skills"
run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh"
- name: Sample files and load memory
- run: "mkdir -p /tmp/gh-aw/agent\n# Sample documentation files (eliminates agent exploratory find turns)\nfind docs/src/content/docs \\( -name '*.md' -o -name '*.mdx' \\) | shuf -n 2 > /tmp/gh-aw/agent/doc-samples.txt\n# Sample workflows with messages (pre-compute instead of agent grep)\ngrep -rl \"messages:\" .github/workflows/ --include=\"*.md\" | shuf -n 2 > /tmp/gh-aw/agent/workflow-samples.txt\n# Sample validation files\nfind pkg -name '*validation*.go' | shuf -n 1 > /tmp/gh-aw/agent/validation-sample.txt || echo \"No validation files found\" > /tmp/gh-aw/agent/validation-sample.txt\n# Load historical memory (eliminates agent memory-read turns)\ncat memory/delight/previous-findings.json 2>/dev/null > /tmp/gh-aw/agent/previous-findings.json || echo \"[]\" > /tmp/gh-aw/agent/previous-findings.json\ncat memory/delight/improvement-themes.json 2>/dev/null > /tmp/gh-aw/agent/improvement-themes.json || echo \"[]\" > /tmp/gh-aw/agent/improvement-themes.json\n"
+ run: "mkdir -p /tmp/gh-aw/agent\n# Sample documentation files (eliminates agent exploratory find turns)\nfind docs/src/content/docs \\( -name '*.md' -o -name '*.mdx' \\) | shuf -n 2 > /tmp/gh-aw/agent/doc-samples.txt\n# Sample workflows with messages (pre-compute instead of agent grep)\ngrep -rl \"messages:\" .github/workflows/ --include=\"*.md\" | shuf -n 2 > /tmp/gh-aw/agent/workflow-samples.txt\n# Sample validation files\nfind pkg -name '*validation*.go' | shuf -n 1 > /tmp/gh-aw/agent/validation-sample.txt || echo \"No validation files found\" > /tmp/gh-aw/agent/validation-sample.txt\n# Load historical memory (eliminates agent memory-read turns)\ncat memory/delight/previous-findings.json 2>/dev/null > /tmp/gh-aw/agent/previous-findings.json || echo \"[]\" > /tmp/gh-aw/agent/previous-findings.json\ncat memory/delight/improvement-themes.json 2>/dev/null > /tmp/gh-aw/agent/improvement-themes.json || echo \"[]\" > /tmp/gh-aw/agent/improvement-themes.json"
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.24@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4
diff --git a/.github/workflows/dependabot-burner.lock.yml b/.github/workflows/dependabot-burner.lock.yml
index 442b31a476b..8b5501b7208 100644
--- a/.github/workflows/dependabot-burner.lock.yml
+++ b/.github/workflows/dependabot-burner.lock.yml
@@ -552,7 +552,7 @@ jobs:
name: Prefetch dependabot burner context
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
- script: "const fs = require('fs');\nconst path = require('path');\n\nconst manifestTargets = new Set([\n '.github/workflows/package.json',\n '.github/workflows/package-lock.json',\n '.github/workflows/requirements.txt',\n '.github/workflows/go.mod',\n]);\nconst objective = (process.env.BURN_OBJECTIVE || '').trim() || 'Close grouped Dependabot PRs for generated workflow manifests by updating source workflow markdown and recompiling in one replacement PR.';\nconst outPath = '/tmp/gh-aw/agent/dependabot-burner/context.json';\n\nfunction parseBumpTitle(title) {\n const match = String(title || '').match(/^Bump\\s+(.+?)\\s+from\\s+([^\\s]+)\\s+to\\s+([^\\s]+)$/i);\n if (!match) {\n return {\n dependency_name: String(title || '').trim(),\n current_version: '',\n target_version: '',\n title_parse_mode: 'fallback',\n };\n }\n return {\n dependency_name: match[1],\n current_version: match[2],\n target_version: match[3],\n title_parse_mode: 'parsed',\n };\n}\n\nfunction normalizeManifestFamily(filename) {\n if (filename.includes('package')) {\n return 'npm';\n }\n if (filename.endsWith('requirements.txt')) {\n return 'pip';\n }\n if (filename.endsWith('go.mod')) {\n return 'go';\n }\n return 'other';\n}\n\nfunction summarizeFamilies(files) {\n return [...new Set((files || []).map(normalizeManifestFamily))].sort();\n}\n\nfunction getTriggerPRNumber() {\n if (context.payload.pull_request?.number) {\n return Number(context.payload.pull_request.number);\n }\n if (context.payload.issue?.pull_request && context.payload.issue?.number) {\n return Number(context.payload.issue.number);\n }\n return null;\n}\n\nasync function loadPullFiles(pullNumber) {\n const files = await github.paginate(github.rest.pulls.listFiles, {\n owner: context.repo.owner,\n repo: context.repo.repo,\n pull_number: pullNumber,\n per_page: 100,\n });\n return files.map((file) => file.filename).filter((filename) => manifestTargets.has(filename));\n}\n\nasync function listOpenDependabotPRs() {\n const pulls = await github.paginate(github.rest.pulls.list, {\n owner: context.repo.owner,\n repo: context.repo.repo,\n state: 'open',\n per_page: 100,\n });\n\n const candidates = [];\n for (const pull of pulls) {\n const author = pull.user?.login || '';\n if (author !== 'dependabot[bot]' && author !== 'app/dependabot') {\n continue;\n }\n\n const manifestFiles = await loadPullFiles(pull.number);\n if (manifestFiles.length === 0) {\n continue;\n }\n\n const parsed = parseBumpTitle(pull.title);\n candidates.push({\n number: pull.number,\n title: pull.title,\n dependency_name: parsed.dependency_name,\n current_version: parsed.current_version,\n target_version: parsed.target_version,\n title_parse_mode: parsed.title_parse_mode,\n manifest_files: manifestFiles,\n manifest_families: summarizeFamilies(manifestFiles),\n created_at: pull.created_at,\n updated_at: pull.updated_at,\n url: pull.html_url,\n });\n }\n\n return candidates.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());\n}\n\nasync function listRecentClosedBurnerPRs() {\n const pulls = await github.paginate(github.rest.pulls.list, {\n owner: context.repo.owner,\n repo: context.repo.repo,\n state: 'closed',\n per_page: 100,\n });\n\n return pulls\n .filter((pull) => pull.title?.startsWith('[dependabot-burner] ') && !pull.merged_at)\n .slice(0, 20)\n .map((pull) => ({\n number: pull.number,\n title: pull.title,\n body: pull.body || '',\n url: pull.html_url,\n closed_at: pull.closed_at,\n created_at: pull.created_at,\n }))\n .sort((a, b) => new Date(b.closed_at || b.created_at).getTime() - new Date(a.closed_at || a.created_at).getTime());\n}\n\nconst triggerPRNumber = getTriggerPRNumber();\nconst openPRs = await listOpenDependabotPRs();\nconst recentFailedBurns = await listRecentClosedBurnerPRs();\n\nlet triggerPR = openPRs.find((pull) => pull.number === triggerPRNumber) || null;\nlet selectionReason = triggerPRNumber ? 'slash-command-trigger-not-in-scope' : 'bundle-all-open-manifest-prs';\nlet selectedPRs = openPRs;\n\nif (triggerPRNumber) {\n if (!triggerPR) {\n const manifestFiles = await loadPullFiles(triggerPRNumber);\n if (manifestFiles.length > 0) {\n const pull = await github.rest.pulls.get({\n owner: context.repo.owner,\n repo: context.repo.repo,\n pull_number: triggerPRNumber,\n });\n const parsed = parseBumpTitle(pull.data.title);\n triggerPR = {\n number: pull.data.number,\n title: pull.data.title,\n dependency_name: parsed.dependency_name,\n current_version: parsed.current_version,\n target_version: parsed.target_version,\n title_parse_mode: parsed.title_parse_mode,\n manifest_files: manifestFiles,\n manifest_families: summarizeFamilies(manifestFiles),\n created_at: pull.data.created_at,\n updated_at: pull.data.updated_at,\n url: pull.data.html_url,\n };\n }\n }\n\n if (triggerPR) {\n const triggerFiles = new Set(triggerPR.manifest_files || []);\n selectedPRs = openPRs.filter((pull) => {\n if (pull.number === triggerPR.number) {\n return true;\n }\n return (pull.manifest_files || []).some((file) => triggerFiles.has(file));\n });\n selectionReason = 'slash-command-similar-prs';\n } else {\n selectedPRs = [];\n }\n}\n\nconst payload = {\n objective,\n trigger_event: context.eventName,\n trigger_pr_number: triggerPRNumber,\n trigger_pr: triggerPR,\n selection_reason: selectionReason,\n open_pr_count: openPRs.length,\n selected_batch_pr_numbers: selectedPRs.map((pull) => pull.number),\n selected_batch_dependencies: selectedPRs.map((pull) => ({\n pr_number: pull.number,\n dependency_name: pull.dependency_name,\n current_version: pull.current_version,\n target_version: pull.target_version,\n title_parse_mode: pull.title_parse_mode,\n manifest_files: pull.manifest_files,\n manifest_families: pull.manifest_families,\n title: pull.title,\n url: pull.url,\n })),\n related_prs: triggerPRNumber ? selectedPRs.filter((pull) => pull.number !== triggerPRNumber) : [],\n recent_failed_burns: recentFailedBurns,\n};\n\nfs.mkdirSync(path.dirname(outPath), { recursive: true });\nfs.writeFileSync(outPath, JSON.stringify(payload, null, 2) + '\\n', 'utf8');\nconsole.log(JSON.stringify(payload, null, 2));\n"
+ script: "const fs = require('fs');\nconst path = require('path');\n\nconst manifestTargets = new Set([\n '.github/workflows/package.json',\n '.github/workflows/package-lock.json',\n '.github/workflows/requirements.txt',\n '.github/workflows/go.mod',\n]);\nconst objective = (process.env.BURN_OBJECTIVE || '').trim() || 'Close grouped Dependabot PRs for generated workflow manifests by updating source workflow markdown and recompiling in one replacement PR.';\nconst outPath = '/tmp/gh-aw/agent/dependabot-burner/context.json';\n\nfunction parseBumpTitle(title) {\n const match = String(title || '').match(/^Bump\\s+(.+?)\\s+from\\s+([^\\s]+)\\s+to\\s+([^\\s]+)$/i);\n if (!match) {\n return {\n dependency_name: String(title || '').trim(),\n current_version: '',\n target_version: '',\n title_parse_mode: 'fallback',\n };\n }\n return {\n dependency_name: match[1],\n current_version: match[2],\n target_version: match[3],\n title_parse_mode: 'parsed',\n };\n}\n\nfunction normalizeManifestFamily(filename) {\n if (filename.includes('package')) {\n return 'npm';\n }\n if (filename.endsWith('requirements.txt')) {\n return 'pip';\n }\n if (filename.endsWith('go.mod')) {\n return 'go';\n }\n return 'other';\n}\n\nfunction summarizeFamilies(files) {\n return [...new Set((files || []).map(normalizeManifestFamily))].sort();\n}\n\nfunction getTriggerPRNumber() {\n if (context.payload.pull_request?.number) {\n return Number(context.payload.pull_request.number);\n }\n if (context.payload.issue?.pull_request && context.payload.issue?.number) {\n return Number(context.payload.issue.number);\n }\n return null;\n}\n\nasync function loadPullFiles(pullNumber) {\n const files = await github.paginate(github.rest.pulls.listFiles, {\n owner: context.repo.owner,\n repo: context.repo.repo,\n pull_number: pullNumber,\n per_page: 100,\n });\n return files.map((file) => file.filename).filter((filename) => manifestTargets.has(filename));\n}\n\nasync function listOpenDependabotPRs() {\n const pulls = await github.paginate(github.rest.pulls.list, {\n owner: context.repo.owner,\n repo: context.repo.repo,\n state: 'open',\n per_page: 100,\n });\n\n const candidates = [];\n for (const pull of pulls) {\n const author = pull.user?.login || '';\n if (author !== 'dependabot[bot]' && author !== 'app/dependabot') {\n continue;\n }\n\n const manifestFiles = await loadPullFiles(pull.number);\n if (manifestFiles.length === 0) {\n continue;\n }\n\n const parsed = parseBumpTitle(pull.title);\n candidates.push({\n number: pull.number,\n title: pull.title,\n dependency_name: parsed.dependency_name,\n current_version: parsed.current_version,\n target_version: parsed.target_version,\n title_parse_mode: parsed.title_parse_mode,\n manifest_files: manifestFiles,\n manifest_families: summarizeFamilies(manifestFiles),\n created_at: pull.created_at,\n updated_at: pull.updated_at,\n url: pull.html_url,\n });\n }\n\n return candidates.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());\n}\n\nasync function listRecentClosedBurnerPRs() {\n const pulls = await github.paginate(github.rest.pulls.list, {\n owner: context.repo.owner,\n repo: context.repo.repo,\n state: 'closed',\n per_page: 100,\n });\n\n return pulls\n .filter((pull) => pull.title?.startsWith('[dependabot-burner] ') && !pull.merged_at)\n .slice(0, 20)\n .map((pull) => ({\n number: pull.number,\n title: pull.title,\n body: pull.body || '',\n url: pull.html_url,\n closed_at: pull.closed_at,\n created_at: pull.created_at,\n }))\n .sort((a, b) => new Date(b.closed_at || b.created_at).getTime() - new Date(a.closed_at || a.created_at).getTime());\n}\n\nconst triggerPRNumber = getTriggerPRNumber();\nconst openPRs = await listOpenDependabotPRs();\nconst recentFailedBurns = await listRecentClosedBurnerPRs();\n\nlet triggerPR = openPRs.find((pull) => pull.number === triggerPRNumber) || null;\nlet selectionReason = triggerPRNumber ? 'slash-command-trigger-not-in-scope' : 'bundle-all-open-manifest-prs';\nlet selectedPRs = openPRs;\n\nif (triggerPRNumber) {\n if (!triggerPR) {\n const manifestFiles = await loadPullFiles(triggerPRNumber);\n if (manifestFiles.length > 0) {\n const pull = await github.rest.pulls.get({\n owner: context.repo.owner,\n repo: context.repo.repo,\n pull_number: triggerPRNumber,\n });\n const parsed = parseBumpTitle(pull.data.title);\n triggerPR = {\n number: pull.data.number,\n title: pull.data.title,\n dependency_name: parsed.dependency_name,\n current_version: parsed.current_version,\n target_version: parsed.target_version,\n title_parse_mode: parsed.title_parse_mode,\n manifest_files: manifestFiles,\n manifest_families: summarizeFamilies(manifestFiles),\n created_at: pull.data.created_at,\n updated_at: pull.data.updated_at,\n url: pull.data.html_url,\n };\n }\n }\n\n if (triggerPR) {\n const triggerFiles = new Set(triggerPR.manifest_files || []);\n selectedPRs = openPRs.filter((pull) => {\n if (pull.number === triggerPR.number) {\n return true;\n }\n return (pull.manifest_files || []).some((file) => triggerFiles.has(file));\n });\n selectionReason = 'slash-command-similar-prs';\n } else {\n selectedPRs = [];\n }\n}\n\nconst payload = {\n objective,\n trigger_event: context.eventName,\n trigger_pr_number: triggerPRNumber,\n trigger_pr: triggerPR,\n selection_reason: selectionReason,\n open_pr_count: openPRs.length,\n selected_batch_pr_numbers: selectedPRs.map((pull) => pull.number),\n selected_batch_dependencies: selectedPRs.map((pull) => ({\n pr_number: pull.number,\n dependency_name: pull.dependency_name,\n current_version: pull.current_version,\n target_version: pull.target_version,\n title_parse_mode: pull.title_parse_mode,\n manifest_files: pull.manifest_files,\n manifest_families: pull.manifest_families,\n title: pull.title,\n url: pull.url,\n })),\n related_prs: triggerPRNumber ? selectedPRs.filter((pull) => pull.number !== triggerPRNumber) : [],\n recent_failed_burns: recentFailedBurns,\n};\n\nfs.mkdirSync(path.dirname(outPath), { recursive: true });\nfs.writeFileSync(outPath, JSON.stringify(payload, null, 2) + '\\n', 'utf8');\nconsole.log(JSON.stringify(payload, null, 2));"
# Cache configuration from frontmatter processed below
- name: Dependabot burner selection context
diff --git a/.github/workflows/design-decision-gate.lock.yml b/.github/workflows/design-decision-gate.lock.yml
index df307f82470..45dcbd4276d 100644
--- a/.github/workflows/design-decision-gate.lock.yml
+++ b/.github/workflows/design-decision-gate.lock.yml
@@ -572,7 +572,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
name: Pre-fetch ADR gate PR context
- run: "set -euo pipefail\n\nif [ \"$EXPR_GITHUB_EVENT_NAME\" = \"workflow_dispatch\" ] && [ -z \"${PR_NUMBER:-}\" ]; then\n echo \"::error::workflow_dispatch requires inputs.pr_number\"\n exit 1\nfi\n\nmkdir -p /tmp/gh-aw/agent\n\ngh pr view \"$PR_NUMBER\" \\\n --repo \"$EXPR_GITHUB_REPOSITORY\" \\\n --json number,title,body,labels,baseRefName,headRefName,author,url \\\n > /tmp/gh-aw/agent/pr.json\n\ngh api --paginate \"repos/$EXPR_GITHUB_REPOSITORY/pulls/$PR_NUMBER/files?per_page=100\" \\\n --jq '.[]' | jq -s '.' > /tmp/gh-aw/agent/pr-files.json\n\nFILE_COUNT=$(jq 'length' /tmp/gh-aw/agent/pr-files.json)\n\nif [ \"$FILE_COUNT\" -gt 300 ]; then\n echo \"::warning::PR has $FILE_COUNT changed files (exceeds the 300-file GitHub diff API limit). Skipping full diff; file listing is available in pr-files.json.\"\n printf '# Diff unavailable: PR has %s changed files (exceeds the 300-file GitHub diff API limit).\\n# Use pr-files.json for the full file listing instead.\\n' \"$FILE_COUNT\" \\\n > /tmp/gh-aw/agent/pr.diff\nelse\n gh pr diff \"$PR_NUMBER\" \\\n --repo \"$EXPR_GITHUB_REPOSITORY\" \\\n > /tmp/gh-aw/agent/pr.diff\nfi\n\nif [ -f \"$EXPR_GITHUB_WORKSPACE/.design-gate.yml\" ]; then\n cp \"$EXPR_GITHUB_WORKSPACE/.design-gate.yml\" /tmp/gh-aw/agent/design-gate-config.yml\n HAS_CUSTOM_CONFIG=true\nelse\n echo \"No .design-gate.yml found ā using defaults\" > /tmp/gh-aw/agent/design-gate-config.yml\n HAS_CUSTOM_CONFIG=false\nfi\n\nBUSINESS_ADDITIONS_DEFAULT=$(jq '[.[] | select(.filename | test(\"^(src|lib|pkg|internal|app|core|domain|services|api)/\")) | .additions] | add // 0' /tmp/gh-aw/agent/pr-files.json)\nHAS_IMPLEMENTATION_LABEL=$(jq '[.labels[]?.name] | index(\"implementation\") != null' /tmp/gh-aw/agent/pr.json)\n\njq -n \\\n --argjson default_business_additions \"$BUSINESS_ADDITIONS_DEFAULT\" \\\n --argjson has_implementation_label \"$HAS_IMPLEMENTATION_LABEL\" \\\n --argjson has_custom_config \"$HAS_CUSTOM_CONFIG\" \\\n --arg pr_number \"$PR_NUMBER\" \\\n --arg threshold \"100\" \\\n --argjson file_count \"$FILE_COUNT\" \\\n --argjson diff_available \"$(jq -n --argjson fc \"$FILE_COUNT\" 'if $fc <= 300 then true else false end')\" \\\n '{\n pr_number: ($pr_number | tonumber),\n threshold: ($threshold | tonumber),\n has_custom_config: $has_custom_config,\n has_implementation_label: $has_implementation_label,\n default_business_additions: $default_business_additions,\n requires_adr_by_default_volume: ($default_business_additions > ($threshold | tonumber)),\n file_count: $file_count,\n diff_available: $diff_available\n }' > /tmp/gh-aw/agent/adr-prefetch-summary.json\n"
+ run: "set -euo pipefail\n\nif [ \"$EXPR_GITHUB_EVENT_NAME\" = \"workflow_dispatch\" ] && [ -z \"${PR_NUMBER:-}\" ]; then\n echo \"::error::workflow_dispatch requires inputs.pr_number\"\n exit 1\nfi\n\nmkdir -p /tmp/gh-aw/agent\n\ngh pr view \"$PR_NUMBER\" \\\n --repo \"$EXPR_GITHUB_REPOSITORY\" \\\n --json number,title,body,labels,baseRefName,headRefName,author,url \\\n > /tmp/gh-aw/agent/pr.json\n\ngh api --paginate \"repos/$EXPR_GITHUB_REPOSITORY/pulls/$PR_NUMBER/files?per_page=100\" \\\n --jq '.[]' | jq -s '.' > /tmp/gh-aw/agent/pr-files.json\n\nFILE_COUNT=$(jq 'length' /tmp/gh-aw/agent/pr-files.json)\n\nif [ \"$FILE_COUNT\" -gt 300 ]; then\n echo \"::warning::PR has $FILE_COUNT changed files (exceeds the 300-file GitHub diff API limit). Skipping full diff; file listing is available in pr-files.json.\"\n printf '# Diff unavailable: PR has %s changed files (exceeds the 300-file GitHub diff API limit).\\n# Use pr-files.json for the full file listing instead.\\n' \"$FILE_COUNT\" \\\n > /tmp/gh-aw/agent/pr.diff\nelse\n gh pr diff \"$PR_NUMBER\" \\\n --repo \"$EXPR_GITHUB_REPOSITORY\" \\\n > /tmp/gh-aw/agent/pr.diff\nfi\n\nif [ -f \"$EXPR_GITHUB_WORKSPACE/.design-gate.yml\" ]; then\n cp \"$EXPR_GITHUB_WORKSPACE/.design-gate.yml\" /tmp/gh-aw/agent/design-gate-config.yml\n HAS_CUSTOM_CONFIG=true\nelse\n echo \"No .design-gate.yml found ā using defaults\" > /tmp/gh-aw/agent/design-gate-config.yml\n HAS_CUSTOM_CONFIG=false\nfi\n\nBUSINESS_ADDITIONS_DEFAULT=$(jq '[.[] | select(.filename | test(\"^(src|lib|pkg|internal|app|core|domain|services|api)/\")) | .additions] | add // 0' /tmp/gh-aw/agent/pr-files.json)\nHAS_IMPLEMENTATION_LABEL=$(jq '[.labels[]?.name] | index(\"implementation\") != null' /tmp/gh-aw/agent/pr.json)\n\njq -n \\\n --argjson default_business_additions \"$BUSINESS_ADDITIONS_DEFAULT\" \\\n --argjson has_implementation_label \"$HAS_IMPLEMENTATION_LABEL\" \\\n --argjson has_custom_config \"$HAS_CUSTOM_CONFIG\" \\\n --arg pr_number \"$PR_NUMBER\" \\\n --arg threshold \"100\" \\\n --argjson file_count \"$FILE_COUNT\" \\\n --argjson diff_available \"$(jq -n --argjson fc \"$FILE_COUNT\" 'if $fc <= 300 then true else false end')\" \\\n '{\n pr_number: ($pr_number | tonumber),\n threshold: ($threshold | tonumber),\n has_custom_config: $has_custom_config,\n has_implementation_label: $has_implementation_label,\n default_business_additions: $default_business_additions,\n requires_adr_by_default_volume: ($default_business_additions > ($threshold | tonumber)),\n file_count: $file_count,\n diff_available: $diff_available\n }' > /tmp/gh-aw/agent/adr-prefetch-summary.json"
- name: Configure Git credentials
env:
diff --git a/.github/workflows/designer-drift-audit.lock.yml b/.github/workflows/designer-drift-audit.lock.yml
index d4a8dd3625c..8d1696dff2c 100644
--- a/.github/workflows/designer-drift-audit.lock.yml
+++ b/.github/workflows/designer-drift-audit.lock.yml
@@ -453,7 +453,7 @@ jobs:
- name: Extract designer file metadata
run: "# Skill file\nSKILL=\".github/skills/agentic-workflow-designer/SKILL.md\"\nif [ -f \"$SKILL\" ]; then\n {\n echo \"=== SKILL.md ===\"\n grep -n '^##' \"$SKILL\" | head -50\n echo \"---\"\n # Decision heuristic tables\n grep -E '^\\|.*\\|' \"$SKILL\" | grep -ivE '^\\|[\\s-]+\\|' | head -60\n echo \"---\"\n # Explicit references to .github/aw/ files\n grep -n '\\.github/aw/' \"$SKILL\" || true\n echo \"===\"\n } > /tmp/gh-aw/data/designer-skill.txt\nfi\n\n# Agent file\nAGENT=\".github/agents/interactive-agent-designer.agent.md\"\nif [ -f \"$AGENT\" ]; then\n {\n echo \"=== interactive-agent-designer.agent.md ===\"\n grep -n '^##' \"$AGENT\" | head -50\n echo \"---\"\n grep -E '^\\|.*\\|' \"$AGENT\" | grep -ivE '^\\|[\\s-]+\\|' | head -60\n echo \"---\"\n grep -n '\\.github/aw/' \"$AGENT\" || true\n echo \"===\"\n } > /tmp/gh-aw/data/designer-agent.txt\nfi\n"
- name: Collect recent changes to reference docs
- run: "# Commits in the last 7 days that touched reference docs\ngit log --oneline --since=\"7 days ago\" -- \\\n .github/aw/syntax.md \\\n .github/aw/safe-outputs.md \\\n .github/aw/network.md \\\n .github/aw/patterns.md \\\n .github/aw/subagents.md \\\n .github/aw/token-optimization.md \\\n .github/aw/triggers.md \\\n .github/aw/create-agentic-workflow.md \\\n > /tmp/gh-aw/data/recent-ref-commits.txt 2>/dev/null || true\n\n# Commits in the last 7 days that touched designer files\ngit log --oneline --since=\"7 days ago\" -- \\\n .github/skills/agentic-workflow-designer/SKILL.md \\\n .github/agents/interactive-agent-designer.agent.md \\\n > /tmp/gh-aw/data/recent-designer-commits.txt 2>/dev/null || true\n\n# New or changed safe output types (from validation config in Go)\ngrep -oP '\"(\\w+)\":\\s*\\{' pkg/workflow/safe_outputs_validation_config.go \\\n | sed 's/\": {//' | tr -d '\"' | sort \\\n > /tmp/gh-aw/data/all-safe-output-types.txt 2>/dev/null || true\n"
+ run: "# Commits in the last 7 days that touched reference docs\ngit log --oneline --since=\"7 days ago\" -- \\\n .github/aw/syntax.md \\\n .github/aw/safe-outputs.md \\\n .github/aw/network.md \\\n .github/aw/patterns.md \\\n .github/aw/subagents.md \\\n .github/aw/token-optimization.md \\\n .github/aw/triggers.md \\\n .github/aw/create-agentic-workflow.md \\\n > /tmp/gh-aw/data/recent-ref-commits.txt 2>/dev/null || true\n\n# Commits in the last 7 days that touched designer files\ngit log --oneline --since=\"7 days ago\" -- \\\n .github/skills/agentic-workflow-designer/SKILL.md \\\n .github/agents/interactive-agent-designer.agent.md \\\n > /tmp/gh-aw/data/recent-designer-commits.txt 2>/dev/null || true\n\n# New or changed safe output types (from validation config in Go)\ngrep -oP '\"(\\w+)\":\\s*\\{' pkg/workflow/safe_outputs_validation_config.go \\\n | sed 's/\": {//' | tr -d '\"' | sort \\\n > /tmp/gh-aw/data/all-safe-output-types.txt 2>/dev/null || true"
- name: Configure Git credentials
env:
diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml
index 2fa3bdc6938..8189b40dc2a 100644
--- a/.github/workflows/docs-noob-tester.lock.yml
+++ b/.github/workflows/docs-noob-tester.lock.yml
@@ -557,7 +557,7 @@ jobs:
- name: Wait for server readiness
run: "MAX_WAIT=135 # 45 attempts Ć 3s = 135s max wait\nWAITED=0\nuntil (echo > /dev/tcp/127.0.0.1/4321) > /dev/null 2>&1; do\n # Check if the server process has already died\n if [ -f /tmp/gh-aw/agent/server.pid ] && ! kill -0 \"$(cat /tmp/gh-aw/agent/server.pid)\" 2>/dev/null; then\n echo \"::error::Documentation server process died before opening port 4321. Server log:\"\n cat /tmp/gh-aw/agent/preview.log\n exit 1\n fi\n WAITED=$((WAITED + 3))\n if [ $WAITED -ge $MAX_WAIT ]; then\n echo \"::error::Documentation server port 4321 did not open after ${MAX_WAIT}s. Server log:\"\n cat /tmp/gh-aw/agent/preview.log\n exit 1\n fi\n echo \"Waiting for docs port... ($WAITED/${MAX_WAIT}s)\"\n sleep 3\ndone\nWAITED=0\nuntil curl -sf http://localhost:4321/gh-aw/ > /dev/null 2>&1; do\n # Check if the server process has already died\n if [ -f /tmp/gh-aw/agent/server.pid ] && ! kill -0 \"$(cat /tmp/gh-aw/agent/server.pid)\" 2>/dev/null; then\n echo \"::error::Documentation server process died before becoming ready. Server log:\"\n cat /tmp/gh-aw/agent/preview.log\n exit 1\n fi\n WAITED=$((WAITED + 3))\n if [ $WAITED -ge $MAX_WAIT ]; then\n echo \"::error::Documentation server did not start after ${MAX_WAIT}s. Server log:\"\n cat /tmp/gh-aw/agent/preview.log\n exit 1\n fi\n echo \"Waiting for server... ($WAITED/${MAX_WAIT}s)\"\n sleep 3\ndone\necho \"Server ready at http://localhost:4321/gh-aw/!\"\n"
- name: Write server URL for agent
- run: "mkdir -p /tmp/gh-aw/agent\necho \"http://localhost:4321/gh-aw/\" > /tmp/gh-aw/agent/server-url.txt\necho \"Server URL: http://localhost:4321/gh-aw/\"\n"
+ run: "mkdir -p /tmp/gh-aw/agent\necho \"http://localhost:4321/gh-aw/\" > /tmp/gh-aw/agent/server-url.txt\necho \"Server URL: http://localhost:4321/gh-aw/\""
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4
diff --git a/.github/workflows/eslint-monster.lock.yml b/.github/workflows/eslint-monster.lock.yml
index 4a833cb5297..4f0ea2bbfd4 100644
--- a/.github/workflows/eslint-monster.lock.yml
+++ b/.github/workflows/eslint-monster.lock.yml
@@ -473,7 +473,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
- id: eslint_scan
name: Run ESLint factory pre-check
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\nrm -f /tmp/gh-aw/agent/lint-clean.flag\nREPO_ROOT=\"$(pwd)\"\n\ncd eslint-factory\nnpm ci > /tmp/gh-aw/agent/eslint-factory.log 2>&1\n\nif npm run lint:setup-js >> /tmp/gh-aw/agent/eslint-factory.log 2>&1; then\n : > /tmp/gh-aw/agent/eslint-diagnostics.txt\n : > /tmp/gh-aw/agent/skill-index.txt\n touch /tmp/gh-aw/agent/lint-clean.flag\n exit 0\nfi\n\ngrep -E '^[^:]+:[0-9]+:[0-9]+:' /tmp/gh-aw/agent/eslint-factory.log > /tmp/gh-aw/agent/eslint-diagnostics.txt || true\ndiag_count=$(wc -l < /tmp/gh-aw/agent/eslint-diagnostics.txt | tr -d ' ')\nif [ \"${diag_count}\" -eq 0 ]; then\n grep -E '^[[:space:]]*[^[:space:]].*$' /tmp/gh-aw/agent/eslint-factory.log | head -n 80 > /tmp/gh-aw/agent/eslint-diagnostics.txt || true\nfi\n\nfind \"${REPO_ROOT}/.github/skills\" -maxdepth 6 -name 'SKILL.md' | sort > /tmp/gh-aw/agent/skill-index.txt\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\nrm -f /tmp/gh-aw/agent/lint-clean.flag\nREPO_ROOT=\"$(pwd)\"\n\ncd eslint-factory\nnpm ci > /tmp/gh-aw/agent/eslint-factory.log 2>&1\n\nif npm run lint:setup-js >> /tmp/gh-aw/agent/eslint-factory.log 2>&1; then\n : > /tmp/gh-aw/agent/eslint-diagnostics.txt\n : > /tmp/gh-aw/agent/skill-index.txt\n touch /tmp/gh-aw/agent/lint-clean.flag\n exit 0\nfi\n\ngrep -E '^[^:]+:[0-9]+:[0-9]+:' /tmp/gh-aw/agent/eslint-factory.log > /tmp/gh-aw/agent/eslint-diagnostics.txt || true\ndiag_count=$(wc -l < /tmp/gh-aw/agent/eslint-diagnostics.txt | tr -d ' ')\nif [ \"${diag_count}\" -eq 0 ]; then\n grep -E '^[[:space:]]*[^[:space:]].*$' /tmp/gh-aw/agent/eslint-factory.log | head -n 80 > /tmp/gh-aw/agent/eslint-diagnostics.txt || true\nfi\n\nfind \"${REPO_ROOT}/.github/skills\" -maxdepth 6 -name 'SKILL.md' | sort > /tmp/gh-aw/agent/skill-index.txt"
- name: Configure Git credentials
env:
diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml
index af19e7091e7..f56aa642481 100644
--- a/.github/workflows/glossary-maintainer.lock.yml
+++ b/.github/workflows/glossary-maintainer.lock.yml
@@ -534,7 +534,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Fetch recent changes
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\n# Determine scan scope: Monday = full weekly scan, other weekdays = daily\nDAY=$(date +%u)\nif [ \"$DAY\" -eq 1 ]; then\n SINCE=\"7 days ago\"\n SCOPE=\"weekly\"\nelse\n SINCE=\"24 hours ago\"\n SCOPE=\"daily\"\nfi\n\necho \"Scan scope: $SCOPE (since: $SINCE)\"\n\n# Fetch recent commits (all files) ā includes file names for context\ngit log --since=\"$SINCE\" --oneline --name-only \\\n > /tmp/gh-aw/agent/recent-commits.txt\n\n# Fetch commits that touched docs\ngit log --since=\"$SINCE\" --name-only \\\n --format=\"%H %s\" -- 'docs/**/*.md' 'docs/**/*.mdx' \\\n > /tmp/gh-aw/agent/doc-changes.txt\n\necho \"Recent commits: $(wc -l < /tmp/gh-aw/agent/recent-commits.txt)\"\necho \"Doc file changes: $(wc -l < /tmp/gh-aw/agent/doc-changes.txt)\"\necho \"$SCOPE\" > /tmp/gh-aw/agent/scan-scope.txt\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\n# Determine scan scope: Monday = full weekly scan, other weekdays = daily\nDAY=$(date +%u)\nif [ \"$DAY\" -eq 1 ]; then\n SINCE=\"7 days ago\"\n SCOPE=\"weekly\"\nelse\n SINCE=\"24 hours ago\"\n SCOPE=\"daily\"\nfi\n\necho \"Scan scope: $SCOPE (since: $SINCE)\"\n\n# Fetch recent commits (all files) ā includes file names for context\ngit log --since=\"$SINCE\" --oneline --name-only \\\n > /tmp/gh-aw/agent/recent-commits.txt\n\n# Fetch commits that touched docs\ngit log --since=\"$SINCE\" --name-only \\\n --format=\"%H %s\" -- 'docs/**/*.md' 'docs/**/*.mdx' \\\n > /tmp/gh-aw/agent/doc-changes.txt\n\necho \"Recent commits: $(wc -l < /tmp/gh-aw/agent/recent-commits.txt)\"\necho \"Doc file changes: $(wc -l < /tmp/gh-aw/agent/doc-changes.txt)\"\necho \"$SCOPE\" > /tmp/gh-aw/agent/scan-scope.txt"
# Cache memory file share configuration from frontmatter processed below
- name: Create cache-memory directory
diff --git a/.github/workflows/gpclean.lock.yml b/.github/workflows/gpclean.lock.yml
index fb5a0f15a3d..e9a78be3a2f 100644
--- a/.github/workflows/gpclean.lock.yml
+++ b/.github/workflows/gpclean.lock.yml
@@ -529,7 +529,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Download SBOM from GitHub Dependency Graph API
- run: "set -e\necho \"š¦ Downloading SBOM from GitHub Dependency Graph API...\"\n\n# Download SBOM using gh CLI (requires contents: read permission)\ngh api \\\n -H \"Accept: application/vnd.github+json\" \\\n -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n \"/repos/$GITHUB_REPOSITORY/dependency-graph/sbom\" \\\n > /tmp/gh-aw/agent/sbom.json\n\necho \"ā
SBOM downloaded successfully to /tmp/gh-aw/agent/sbom.json\"\n\n# Show SBOM summary\nif command -v jq &> /dev/null; then\n PACKAGE_COUNT=$(jq '.sbom.packages | length' /tmp/gh-aw/agent/sbom.json 2>/dev/null || echo \"unknown\")\n echo \"š SBOM contains ${PACKAGE_COUNT} packages\"\nfi\n"
+ run: "set -e\necho \"š¦ Downloading SBOM from GitHub Dependency Graph API...\"\n\n# Download SBOM using gh CLI (requires contents: read permission)\ngh api \\\n -H \"Accept: application/vnd.github+json\" \\\n -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n \"/repos/$GITHUB_REPOSITORY/dependency-graph/sbom\" \\\n > /tmp/gh-aw/agent/sbom.json\n\necho \"ā
SBOM downloaded successfully to /tmp/gh-aw/agent/sbom.json\"\n\n# Show SBOM summary\nif command -v jq &> /dev/null; then\n PACKAGE_COUNT=$(jq '.sbom.packages | length' /tmp/gh-aw/agent/sbom.json 2>/dev/null || echo \"unknown\")\n echo \"š SBOM contains ${PACKAGE_COUNT} packages\"\nfi"
# Cache memory file share configuration from frontmatter processed below
- name: Create cache-memory directory
diff --git a/.github/workflows/impeccable-skills-reviewer.lock.yml b/.github/workflows/impeccable-skills-reviewer.lock.yml
index 95827d1eea9..5c3bcf85714 100644
--- a/.github/workflows/impeccable-skills-reviewer.lock.yml
+++ b/.github/workflows/impeccable-skills-reviewer.lock.yml
@@ -558,7 +558,7 @@ jobs:
PR_DIFF_MAX_LINES: "3000"
PR_NUMBER: ${{ github.event.pull_request.number }}
name: Pre-fetch PR diff
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n{ gh pr diff \"$PR_NUMBER\" --repo $EXPR_GITHUB_REPOSITORY \\\n --exclude '**/*.lock.yml' \\\n --exclude '**/generated/**' \\\n --exclude '**/dist/**' \\\n --exclude '**/build/**' \\\n || true; } | head -n \"${PR_DIFF_MAX_LINES}\" > /tmp/gh-aw/agent/pr-diff.patch\nLINES=$(wc -l < /tmp/gh-aw/agent/pr-diff.patch)\ngh pr view \"$PR_NUMBER\" \\\n --repo $EXPR_GITHUB_REPOSITORY \\\n --json number,title,body,headRefName,additions,deletions,changedFiles,files \\\n > /tmp/gh-aw/agent/pr-meta.json\necho \"Pre-fetched PR diff (${LINES} lines) and metadata\"\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n{ gh pr diff \"$PR_NUMBER\" --repo $EXPR_GITHUB_REPOSITORY \\\n --exclude '**/*.lock.yml' \\\n --exclude '**/generated/**' \\\n --exclude '**/dist/**' \\\n --exclude '**/build/**' \\\n || true; } | head -n \"${PR_DIFF_MAX_LINES}\" > /tmp/gh-aw/agent/pr-diff.patch\nLINES=$(wc -l < /tmp/gh-aw/agent/pr-diff.patch)\ngh pr view \"$PR_NUMBER\" \\\n --repo $EXPR_GITHUB_REPOSITORY \\\n --json number,title,body,headRefName,additions,deletions,changedFiles,files \\\n > /tmp/gh-aw/agent/pr-meta.json\necho \"Pre-fetched PR diff (${LINES} lines) and metadata\""
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.24@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4
diff --git a/.github/workflows/issue-arborist.lock.yml b/.github/workflows/issue-arborist.lock.yml
index 0ad951403dc..d34c6dee11b 100644
--- a/.github/workflows/issue-arborist.lock.yml
+++ b/.github/workflows/issue-arborist.lock.yml
@@ -531,7 +531,7 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/start_difc_proxy.sh"
- name: Fetch issues
- run: |
+ run: |-
# Create output directory
mkdir -p /tmp/gh-aw/agent/issues-data
diff --git a/.github/workflows/lint-monster.lock.yml b/.github/workflows/lint-monster.lock.yml
index ca9fe669bcd..63ed27361c8 100644
--- a/.github/workflows/lint-monster.lock.yml
+++ b/.github/workflows/lint-monster.lock.yml
@@ -468,7 +468,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
- id: lint_scan
name: Run custom lint pre-check
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\nrm -f /tmp/gh-aw/agent/lint-clean.flag\n\nif make golint-custom > /tmp/gh-aw/agent/golint-custom.log 2>&1; then\n : > /tmp/gh-aw/agent/lint-diagnostics.txt\n : > /tmp/gh-aw/agent/skill-index.txt\n touch /tmp/gh-aw/agent/lint-clean.flag\n exit 0\nfi\n\ngrep -E '^[^:]+:[0-9]+:[0-9]+:' /tmp/gh-aw/agent/golint-custom.log > /tmp/gh-aw/agent/lint-diagnostics.txt || true\ndiag_count=$(wc -l < /tmp/gh-aw/agent/lint-diagnostics.txt | tr -d ' ')\nif [ \"${diag_count}\" -eq 0 ]; then\n grep -E '^[[:space:]]*[^[:space:]].*$' /tmp/gh-aw/agent/golint-custom.log | head -n 50 > /tmp/gh-aw/agent/lint-diagnostics.txt || true\n diag_count=$(wc -l < /tmp/gh-aw/agent/lint-diagnostics.txt | tr -d ' ')\nfi\n\nfind .github/skills -maxdepth 6 -name 'SKILL.md' | sort > /tmp/gh-aw/agent/skill-index.txt\necho \"Lint diagnostics captured: ${diag_count}\"\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\nrm -f /tmp/gh-aw/agent/lint-clean.flag\n\nif make golint-custom > /tmp/gh-aw/agent/golint-custom.log 2>&1; then\n : > /tmp/gh-aw/agent/lint-diagnostics.txt\n : > /tmp/gh-aw/agent/skill-index.txt\n touch /tmp/gh-aw/agent/lint-clean.flag\n exit 0\nfi\n\ngrep -E '^[^:]+:[0-9]+:[0-9]+:' /tmp/gh-aw/agent/golint-custom.log > /tmp/gh-aw/agent/lint-diagnostics.txt || true\ndiag_count=$(wc -l < /tmp/gh-aw/agent/lint-diagnostics.txt | tr -d ' ')\nif [ \"${diag_count}\" -eq 0 ]; then\n grep -E '^[[:space:]]*[^[:space:]].*$' /tmp/gh-aw/agent/golint-custom.log | head -n 50 > /tmp/gh-aw/agent/lint-diagnostics.txt || true\n diag_count=$(wc -l < /tmp/gh-aw/agent/lint-diagnostics.txt | tr -d ' ')\nfi\n\nfind .github/skills -maxdepth 6 -name 'SKILL.md' | sort > /tmp/gh-aw/agent/skill-index.txt\necho \"Lint diagnostics captured: ${diag_count}\""
- name: Configure Git credentials
env:
diff --git a/.github/workflows/linter-miner.lock.yml b/.github/workflows/linter-miner.lock.yml
index 2730b9f8134..a0b85e60737 100644
--- a/.github/workflows/linter-miner.lock.yml
+++ b/.github/workflows/linter-miner.lock.yml
@@ -566,7 +566,7 @@ jobs:
GH_AW_SKILL_DIR: ".github/skills"
run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh"
- name: Preload linter source and cache context
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\n: > /tmp/gh-aw/agent/linters-src.txt\nwhile IFS= read -r -d '' file; do\n printf '\\n===== FILE: %s =====\\n' \"$file\" >> /tmp/gh-aw/agent/linters-src.txt\n cat \"$file\" >> /tmp/gh-aw/agent/linters-src.txt\ndone < <(find pkg/linters -type f -name '*.go' -print0 | sort -z)\ncat .github/skills/go-linters/SKILL.md > /tmp/gh-aw/agent/go-linters-skill.txt\n\nprior_file=\"\"\nif [ -d /tmp/gh-aw/cache-memory ]; then\n prior_file=\"$(find /tmp/gh-aw/cache-memory -maxdepth 4 -type f -name 'proposed-linters.json' | sort | head -n 1 || true)\"\n if [ -z \"${prior_file}\" ]; then\n prior_file=\"$(find /tmp/gh-aw/cache-memory -maxdepth 4 -type f -name 'proposed-linters' | sort | head -n 1 || true)\"\n fi\nfi\nif [ -n \"${prior_file}\" ] && [ -f \"${prior_file}\" ]; then\n cp \"${prior_file}\" /tmp/gh-aw/agent/prior-linters.json\nelse\n echo \"[]\" > /tmp/gh-aw/agent/prior-linters.json\nfi\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\n: > /tmp/gh-aw/agent/linters-src.txt\nwhile IFS= read -r -d '' file; do\n printf '\\n===== FILE: %s =====\\n' \"$file\" >> /tmp/gh-aw/agent/linters-src.txt\n cat \"$file\" >> /tmp/gh-aw/agent/linters-src.txt\ndone < <(find pkg/linters -type f -name '*.go' -print0 | sort -z)\ncat .github/skills/go-linters/SKILL.md > /tmp/gh-aw/agent/go-linters-skill.txt\n\nprior_file=\"\"\nif [ -d /tmp/gh-aw/cache-memory ]; then\n prior_file=\"$(find /tmp/gh-aw/cache-memory -maxdepth 4 -type f -name 'proposed-linters.json' | sort | head -n 1 || true)\"\n if [ -z \"${prior_file}\" ]; then\n prior_file=\"$(find /tmp/gh-aw/cache-memory -maxdepth 4 -type f -name 'proposed-linters' | sort | head -n 1 || true)\"\n fi\nfi\nif [ -n \"${prior_file}\" ] && [ -f \"${prior_file}\" ]; then\n cp \"${prior_file}\" /tmp/gh-aw/agent/prior-linters.json\nelse\n echo \"[]\" > /tmp/gh-aw/agent/prior-linters.json\nfi"
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.24@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5
diff --git a/.github/workflows/mattpocock-skills-reviewer.lock.yml b/.github/workflows/mattpocock-skills-reviewer.lock.yml
index 3cabcf0a1ae..926908661cb 100644
--- a/.github/workflows/mattpocock-skills-reviewer.lock.yml
+++ b/.github/workflows/mattpocock-skills-reviewer.lock.yml
@@ -705,7 +705,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
name: Pre-fetch PR diff
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n{ gh pr diff \"$PR_NUMBER\" --repo $EXPR_GITHUB_REPOSITORY \\\n --exclude '**/*.lock.yml' \\\n --exclude '**/generated/**' \\\n --exclude '**/dist/**' \\\n --exclude '**/build/**' \\\n || true; } | head -n 3000 > /tmp/gh-aw/agent/pr-diff.patch\nLINES=$(wc -l < /tmp/gh-aw/agent/pr-diff.patch)\ngh pr view \"$PR_NUMBER\" \\\n --repo $EXPR_GITHUB_REPOSITORY \\\n --json number,title,body,headRefName,additions,deletions,changedFiles,files \\\n > /tmp/gh-aw/agent/pr-meta.json\necho \"Pre-fetched PR diff (${LINES} lines) and metadata\"\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n{ gh pr diff \"$PR_NUMBER\" --repo $EXPR_GITHUB_REPOSITORY \\\n --exclude '**/*.lock.yml' \\\n --exclude '**/generated/**' \\\n --exclude '**/dist/**' \\\n --exclude '**/build/**' \\\n || true; } | head -n 3000 > /tmp/gh-aw/agent/pr-diff.patch\nLINES=$(wc -l < /tmp/gh-aw/agent/pr-diff.patch)\ngh pr view \"$PR_NUMBER\" \\\n --repo $EXPR_GITHUB_REPOSITORY \\\n --json number,title,body,headRefName,additions,deletions,changedFiles,files \\\n > /tmp/gh-aw/agent/pr-meta.json\necho \"Pre-fetched PR diff (${LINES} lines) and metadata\""
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.24@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4
diff --git a/.github/workflows/mergefest.lock.yml b/.github/workflows/mergefest.lock.yml
index b450c0e9e14..fd32f482178 100644
--- a/.github/workflows/mergefest.lock.yml
+++ b/.github/workflows/mergefest.lock.yml
@@ -535,7 +535,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Configure Git credentials
- run: "git config user.name \"github-actions[bot]\"\ngit config user.email \"github-actions[bot]@users.noreply.github.com\"\n\n# Create .gitignore to exclude workflow YAML files\ncat > /tmp/gh-aw/agent/merge-gitignore << 'EOF'\n# Exclude all .yml files in .github/workflows/\n.github/workflows/*.yml\nEOF\n"
+ run: "git config user.name \"github-actions[bot]\"\ngit config user.email \"github-actions[bot]@users.noreply.github.com\"\n\n# Create .gitignore to exclude workflow YAML files\ncat > /tmp/gh-aw/agent/merge-gitignore << 'EOF'\n# Exclude all .yml files in .github/workflows/\n.github/workflows/*.yml\nEOF"
- name: Configure Git credentials
env:
diff --git a/.github/workflows/objective-impact-report.lock.yml b/.github/workflows/objective-impact-report.lock.yml
index 4be7bdba8b7..de8be6fa7d6 100644
--- a/.github/workflows/objective-impact-report.lock.yml
+++ b/.github/workflows/objective-impact-report.lock.yml
@@ -522,7 +522,7 @@ jobs:
name: Prepare safe-output issue evaluations
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
- script: "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { execFileSync } = require(\"child_process\");\nconst {\n evaluateItem,\n normalizeOutcome,\n readJSONL,\n} = require(path.join(process.env.GITHUB_WORKSPACE || process.cwd(), \"actions/setup/js/evaluate_outcomes.cjs\"));\n\nconst DATA_DIR = \"/tmp/gh-aw/agent/objective-impact-report\";\nconst RUNS_DIR = path.join(DATA_DIR, \"safe-output-runs\");\nconst OUTPUT_JSONL = path.join(DATA_DIR, \"safe-output-issue-evaluations.jsonl\");\nconst OUTPUT_SUMMARY = path.join(DATA_DIR, \"safe-output-issue-summary.json\");\n\nfunction readJSON(filePath, fallback) {\n try {\n return JSON.parse(fs.readFileSync(filePath, \"utf8\"));\n } catch {\n return fallback;\n }\n}\n\nfunction writeFileAtomic(filePath, content) {\n const baseName = path.basename(filePath);\n const parentDir = path.dirname(filePath);\n const tmpDir = fs.mkdtempSync(path.join(parentDir, `.${baseName}.tmp-`));\n const tmpFile = path.join(tmpDir, baseName);\n try {\n try {\n fs.writeFileSync(tmpFile, content);\n } catch (err) {\n throw new Error(`failed to write temp file ${tmpFile}`, { cause: err });\n }\n try {\n fs.renameSync(tmpFile, filePath);\n } catch (err) {\n throw new Error(`failed to rename temp file ${tmpFile} to ${filePath}`, { cause: err });\n }\n } finally {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n }\n}\n\nfunction writeJSON(filePath, value) {\n writeFileAtomic(filePath, JSON.stringify(value, null, 2) + \"\\n\");\n}\n\nfunction gh(args) {\n try {\n return execFileSync(\"gh\", args, { encoding: \"utf8\", stdio: [\"pipe\", \"pipe\", \"pipe\"] }).trim();\n } catch {\n return null;\n }\n}\n\nfunction ensureIssueURL(item, repo) {\n if (item.url || typeof item.number !== \"number\" || !repo) {\n return item;\n }\n return {\n ...item,\n url: `https://github.com/${repo}/issues/${item.number}`,\n };\n}\n\nfunction loadRuns() {\n const workflowLogs = readJSON(path.join(DATA_DIR, \"workflow-logs.json\"), {});\n const runs = Array.isArray(workflowLogs.runs) ? workflowLogs.runs : [];\n return runs\n .map(run => ({\n id: Number(run.id ?? run.databaseId ?? 0),\n workflow_name: run.workflow_name || run.workflowName || \"\",\n aic: run.aic ?? null,\n created_at: run.created_at || run.createdAt || \"\",\n status: run.status || \"\",\n conclusion: run.conclusion || \"\",\n url: run.html_url || run.url || \"\",\n }))\n .filter(run => Number.isInteger(run.id) && run.id > 0);\n}\n\nfunction loadManifest(runDir) {\n const manifestPath = path.join(runDir, \"safe-output-items.jsonl\");\n if (!fs.existsSync(manifestPath)) return [];\n return readJSONL(manifestPath);\n}\n\nfunction downloadManifest(repo, runId, runDir) {\n fs.mkdirSync(runDir, { recursive: true });\n const manifestPath = path.join(runDir, \"safe-output-items.jsonl\");\n if (fs.existsSync(manifestPath) && fs.statSync(manifestPath).size > 0) {\n return true;\n }\n const result = gh([\"run\", \"download\", String(runId), \"--repo\", repo, \"--name\", \"safe-outputs-items\", \"--dir\", runDir]);\n return result !== null && fs.existsSync(manifestPath) && fs.statSync(manifestPath).size > 0;\n}\n\nfunction main() {\n const repo = process.env.EXPR_GITHUB_REPOSITORY || process.env.GITHUB_REPOSITORY || \"\";\n if (!repo) {\n console.error(\"EXPR_GITHUB_REPOSITORY or GITHUB_REPOSITORY is required\");\n process.exit(1);\n }\n\n fs.mkdirSync(DATA_DIR, { recursive: true });\n fs.mkdirSync(RUNS_DIR, { recursive: true });\n\n const runs = loadRuns();\n const rows = [];\n\n for (const run of runs) {\n const runDir = path.join(RUNS_DIR, `run-${run.id}`);\n if (!downloadManifest(repo, run.id, runDir)) {\n continue;\n }\n\n const items = loadManifest(runDir)\n .filter(item => item && (item.type === \"create_issue\" || item.type === \"close_issue\"))\n .map(item => ensureIssueURL(item, item.repo || repo));\n\n for (const item of items) {\n const evalResult = evaluateItem(item, repo);\n const normalized = normalizeOutcome(evalResult.result, evalResult.detail);\n rows.push({\n run_id: run.id,\n workflow_name: run.workflow_name,\n workflow_aic: run.aic,\n workflow_run_created_at: run.created_at,\n workflow_run_url: run.url,\n type: item.type,\n repo: item.repo || repo,\n number: typeof item.number === \"number\" ? item.number : null,\n url: item.url || \"\",\n timestamp: item.timestamp || \"\",\n result: evalResult.result,\n detail: evalResult.detail,\n outcome_status: normalized.outcome_status,\n evidence_strength: normalized.evidence_strength,\n signal: normalized.signal,\n resolution_sec: evalResult.resolution_sec,\n pending_age_sec: evalResult.pending_age_sec,\n comments: evalResult.comments,\n reactions_total: evalResult.reactions_total,\n reactions_positive: evalResult.reactions_positive,\n reactions_negative: evalResult.reactions_negative,\n zero_touch: evalResult.zero_touch,\n });\n }\n }\n\n writeFileAtomic(OUTPUT_JSONL, rows.map(row => JSON.stringify(row)).join(\"\\n\") + (rows.length > 0 ? \"\\n\" : \"\"));\n\n const summary = {\n total_issue_outcomes: rows.length,\n create_issue_count: rows.filter(row => row.type === \"create_issue\").length,\n close_issue_count: rows.filter(row => row.type === \"close_issue\").length,\n accepted_count: rows.filter(row => row.outcome_status === \"accepted\").length,\n rejected_count: rows.filter(row => row.outcome_status === \"rejected\").length,\n pending_count: rows.filter(row => row.outcome_status === \"pending\").length,\n ignored_count: rows.filter(row => row.outcome_status === \"ignored\").length,\n unknown_count: rows.filter(row => row.outcome_status === \"unknown\").length,\n distinct_workflows: [...new Set(rows.map(row => row.workflow_name).filter(Boolean))].length,\n distinct_runs_with_issue_outcomes: [...new Set(rows.map(row => row.run_id))].length,\n };\n writeJSON(OUTPUT_SUMMARY, summary);\n}\n\nmain();\n"
+ script: "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { execFileSync } = require(\"child_process\");\nconst {\n evaluateItem,\n normalizeOutcome,\n readJSONL,\n} = require(path.join(process.env.GITHUB_WORKSPACE || process.cwd(), \"actions/setup/js/evaluate_outcomes.cjs\"));\n\nconst DATA_DIR = \"/tmp/gh-aw/agent/objective-impact-report\";\nconst RUNS_DIR = path.join(DATA_DIR, \"safe-output-runs\");\nconst OUTPUT_JSONL = path.join(DATA_DIR, \"safe-output-issue-evaluations.jsonl\");\nconst OUTPUT_SUMMARY = path.join(DATA_DIR, \"safe-output-issue-summary.json\");\n\nfunction readJSON(filePath, fallback) {\n try {\n return JSON.parse(fs.readFileSync(filePath, \"utf8\"));\n } catch {\n return fallback;\n }\n}\n\nfunction writeFileAtomic(filePath, content) {\n const baseName = path.basename(filePath);\n const parentDir = path.dirname(filePath);\n const tmpDir = fs.mkdtempSync(path.join(parentDir, `.${baseName}.tmp-`));\n const tmpFile = path.join(tmpDir, baseName);\n try {\n try {\n fs.writeFileSync(tmpFile, content);\n } catch (err) {\n throw new Error(`failed to write temp file ${tmpFile}`, { cause: err });\n }\n try {\n fs.renameSync(tmpFile, filePath);\n } catch (err) {\n throw new Error(`failed to rename temp file ${tmpFile} to ${filePath}`, { cause: err });\n }\n } finally {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n }\n}\n\nfunction writeJSON(filePath, value) {\n writeFileAtomic(filePath, JSON.stringify(value, null, 2) + \"\\n\");\n}\n\nfunction gh(args) {\n try {\n return execFileSync(\"gh\", args, { encoding: \"utf8\", stdio: [\"pipe\", \"pipe\", \"pipe\"] }).trim();\n } catch {\n return null;\n }\n}\n\nfunction ensureIssueURL(item, repo) {\n if (item.url || typeof item.number !== \"number\" || !repo) {\n return item;\n }\n return {\n ...item,\n url: `https://github.com/${repo}/issues/${item.number}`,\n };\n}\n\nfunction loadRuns() {\n const workflowLogs = readJSON(path.join(DATA_DIR, \"workflow-logs.json\"), {});\n const runs = Array.isArray(workflowLogs.runs) ? workflowLogs.runs : [];\n return runs\n .map(run => ({\n id: Number(run.id ?? run.databaseId ?? 0),\n workflow_name: run.workflow_name || run.workflowName || \"\",\n aic: run.aic ?? null,\n created_at: run.created_at || run.createdAt || \"\",\n status: run.status || \"\",\n conclusion: run.conclusion || \"\",\n url: run.html_url || run.url || \"\",\n }))\n .filter(run => Number.isInteger(run.id) && run.id > 0);\n}\n\nfunction loadManifest(runDir) {\n const manifestPath = path.join(runDir, \"safe-output-items.jsonl\");\n if (!fs.existsSync(manifestPath)) return [];\n return readJSONL(manifestPath);\n}\n\nfunction downloadManifest(repo, runId, runDir) {\n fs.mkdirSync(runDir, { recursive: true });\n const manifestPath = path.join(runDir, \"safe-output-items.jsonl\");\n if (fs.existsSync(manifestPath) && fs.statSync(manifestPath).size > 0) {\n return true;\n }\n const result = gh([\"run\", \"download\", String(runId), \"--repo\", repo, \"--name\", \"safe-outputs-items\", \"--dir\", runDir]);\n return result !== null && fs.existsSync(manifestPath) && fs.statSync(manifestPath).size > 0;\n}\n\nfunction main() {\n const repo = process.env.EXPR_GITHUB_REPOSITORY || process.env.GITHUB_REPOSITORY || \"\";\n if (!repo) {\n console.error(\"EXPR_GITHUB_REPOSITORY or GITHUB_REPOSITORY is required\");\n process.exit(1);\n }\n\n fs.mkdirSync(DATA_DIR, { recursive: true });\n fs.mkdirSync(RUNS_DIR, { recursive: true });\n\n const runs = loadRuns();\n const rows = [];\n\n for (const run of runs) {\n const runDir = path.join(RUNS_DIR, `run-${run.id}`);\n if (!downloadManifest(repo, run.id, runDir)) {\n continue;\n }\n\n const items = loadManifest(runDir)\n .filter(item => item && (item.type === \"create_issue\" || item.type === \"close_issue\"))\n .map(item => ensureIssueURL(item, item.repo || repo));\n\n for (const item of items) {\n const evalResult = evaluateItem(item, repo);\n const normalized = normalizeOutcome(evalResult.result, evalResult.detail);\n rows.push({\n run_id: run.id,\n workflow_name: run.workflow_name,\n workflow_aic: run.aic,\n workflow_run_created_at: run.created_at,\n workflow_run_url: run.url,\n type: item.type,\n repo: item.repo || repo,\n number: typeof item.number === \"number\" ? item.number : null,\n url: item.url || \"\",\n timestamp: item.timestamp || \"\",\n result: evalResult.result,\n detail: evalResult.detail,\n outcome_status: normalized.outcome_status,\n evidence_strength: normalized.evidence_strength,\n signal: normalized.signal,\n resolution_sec: evalResult.resolution_sec,\n pending_age_sec: evalResult.pending_age_sec,\n comments: evalResult.comments,\n reactions_total: evalResult.reactions_total,\n reactions_positive: evalResult.reactions_positive,\n reactions_negative: evalResult.reactions_negative,\n zero_touch: evalResult.zero_touch,\n });\n }\n }\n\n writeFileAtomic(OUTPUT_JSONL, rows.map(row => JSON.stringify(row)).join(\"\\n\") + (rows.length > 0 ? \"\\n\" : \"\"));\n\n const summary = {\n total_issue_outcomes: rows.length,\n create_issue_count: rows.filter(row => row.type === \"create_issue\").length,\n close_issue_count: rows.filter(row => row.type === \"close_issue\").length,\n accepted_count: rows.filter(row => row.outcome_status === \"accepted\").length,\n rejected_count: rows.filter(row => row.outcome_status === \"rejected\").length,\n pending_count: rows.filter(row => row.outcome_status === \"pending\").length,\n ignored_count: rows.filter(row => row.outcome_status === \"ignored\").length,\n unknown_count: rows.filter(row => row.outcome_status === \"unknown\").length,\n distinct_workflows: [...new Set(rows.map(row => row.workflow_name).filter(Boolean))].length,\n distinct_runs_with_issue_outcomes: [...new Set(rows.map(row => row.run_id))].length,\n };\n writeJSON(OUTPUT_SUMMARY, summary);\n}\n\nmain();"
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.24@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4
diff --git a/.github/workflows/pr-description-caveman.lock.yml b/.github/workflows/pr-description-caveman.lock.yml
index e3611bb4129..e63c783d20b 100644
--- a/.github/workflows/pr-description-caveman.lock.yml
+++ b/.github/workflows/pr-description-caveman.lock.yml
@@ -480,7 +480,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
name: Fetch and chunk PR diff
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent/chunks\n\nEXCLUSIONS=(\n ':!*.lock.yml' ':!*.lock' ':!*-lock.json' ':!yarn.lock'\n ':!go.sum' ':!go.mod'\n ':!*.generated.*' ':!generated/**' ':!vendor/**'\n ':!dist/**' ':!*.min.js' ':!*.min.css'\n)\n\n# Diff stat (always small ā safe to capture in full)\ngit diff \"$BASE_SHA\"...\"$HEAD_SHA\" -- \"${EXCLUSIONS[@]}\" --stat \\\n > /tmp/gh-aw/agent/diff-stat.txt 2>&1 || true\n\n# Commit log\ngit log --oneline \"$BASE_SHA\"..\"$HEAD_SHA\" \\\n > /tmp/gh-aw/agent/commits.txt 2>&1 || true\n\n# Full diff split into 400-line chunks\n# split -l produces chunk_000, chunk_001, ...\ngit diff \"$BASE_SHA\"...\"$HEAD_SHA\" -- \"${EXCLUSIONS[@]}\" \\\n | split -l 400 - /tmp/gh-aw/agent/chunks/chunk_ 2>/dev/null || true\n\n# Record chunk manifest and count so the agent can process deterministically\nls /tmp/gh-aw/agent/chunks/ > /tmp/gh-aw/agent/chunk-manifest.txt\nwc -l < /tmp/gh-aw/agent/chunk-manifest.txt > /tmp/gh-aw/agent/chunk-count.txt\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent/chunks\n\nEXCLUSIONS=(\n ':!*.lock.yml' ':!*.lock' ':!*-lock.json' ':!yarn.lock'\n ':!go.sum' ':!go.mod'\n ':!*.generated.*' ':!generated/**' ':!vendor/**'\n ':!dist/**' ':!*.min.js' ':!*.min.css'\n)\n\n# Diff stat (always small ā safe to capture in full)\ngit diff \"$BASE_SHA\"...\"$HEAD_SHA\" -- \"${EXCLUSIONS[@]}\" --stat \\\n > /tmp/gh-aw/agent/diff-stat.txt 2>&1 || true\n\n# Commit log\ngit log --oneline \"$BASE_SHA\"..\"$HEAD_SHA\" \\\n > /tmp/gh-aw/agent/commits.txt 2>&1 || true\n\n# Full diff split into 400-line chunks\n# split -l produces chunk_000, chunk_001, ...\ngit diff \"$BASE_SHA\"...\"$HEAD_SHA\" -- \"${EXCLUSIONS[@]}\" \\\n | split -l 400 - /tmp/gh-aw/agent/chunks/chunk_ 2>/dev/null || true\n\n# Record chunk manifest and count so the agent can process deterministically\nls /tmp/gh-aw/agent/chunks/ > /tmp/gh-aw/agent/chunk-manifest.txt\nwc -l < /tmp/gh-aw/agent/chunk-manifest.txt > /tmp/gh-aw/agent/chunk-count.txt"
- name: Configure Git credentials
env:
diff --git a/.github/workflows/prompt-clustering-analysis.lock.yml b/.github/workflows/prompt-clustering-analysis.lock.yml
index a267142e5d6..3eab6c36254 100644
--- a/.github/workflows/prompt-clustering-analysis.lock.yml
+++ b/.github/workflows/prompt-clustering-analysis.lock.yml
@@ -569,7 +569,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Download workflow logs for PR analysis
- run: "# Create logs directory\nmkdir -p /tmp/gh-aw/agent/workflow-logs\n\necho \"Downloading workflow logs to extract turn counts...\"\n\n# Download logs for the last 30 days of copilot workflows\n# This will give us the aw_info.json which contains turn counts\n./gh-aw logs --engine copilot --start-date -30d -o /tmp/gh-aw/agent/workflow-logs\n\n# Verify logs were downloaded\necho \"Downloaded workflow logs:\"\nfind /tmp/gh-aw/agent/workflow-logs -maxdepth 1 -ls\n"
+ run: "# Create logs directory\nmkdir -p /tmp/gh-aw/agent/workflow-logs\n\necho \"Downloading workflow logs to extract turn counts...\"\n\n# Download logs for the last 30 days of copilot workflows\n# This will give us the aw_info.json which contains turn counts\n./gh-aw logs --engine copilot --start-date -30d -o /tmp/gh-aw/agent/workflow-logs\n\n# Verify logs were downloaded\necho \"Downloaded workflow logs:\"\nfind /tmp/gh-aw/agent/workflow-logs -maxdepth 1 -ls"
# Cache configuration from frontmatter processed below
- name: Save prompt clustering data to cache
diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml
index d06c96072a6..cbe60cf6d1e 100644
--- a/.github/workflows/release.lock.yml
+++ b/.github/workflows/release.lock.yml
@@ -502,7 +502,7 @@ jobs:
RELEASE_ID: ${{ needs.release.outputs.release_id }}
RELEASE_TAG: ${{ needs.config.outputs.release_tag }}
name: Setup release environment
- run: "set -e\nmkdir -p /tmp/gh-aw/agent/release-data\nmkdir -p /tmp/gh-aw/agent/community-data\n# Copy community issues from the agent/community-data path (written by community-attribution import step)\ncp /tmp/gh-aw/agent/community-data/community_issues.json /tmp/gh-aw/agent/community-data/community_issues.json 2>/dev/null || echo \"[]\" > /tmp/gh-aw/agent/community-data/community_issues.json\n\n# Use the release ID and tag from the release job\necho \"Release ID from release job: $RELEASE_ID\"\necho \"Release tag from release job: $RELEASE_TAG\"\n\necho \"Processing release: $RELEASE_TAG\"\necho \"RELEASE_TAG=$RELEASE_TAG\" >> \"$GITHUB_ENV\"\n\n# Get the current release information\n# Use release ID to fetch release data\ngh api \"/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID\" > /tmp/gh-aw/agent/release-data/current_release.json\necho \"ā Fetched current release information\"\n\n# Get the previous release to determine the range\nPREV_RELEASE_TAG=$(gh release list --limit 2 --json tagName --jq '.[1].tagName // empty')\n\nif [ -z \"$PREV_RELEASE_TAG\" ]; then\n echo \"No previous release found. This appears to be the first release.\"\n echo \"PREV_RELEASE_TAG=\" >> \"$GITHUB_ENV\"\n touch /tmp/gh-aw/agent/release-data/pull_requests.json\n echo \"[]\" > /tmp/gh-aw/agent/release-data/pull_requests.json\nelse\n echo \"Previous release: $PREV_RELEASE_TAG\"\n echo \"PREV_RELEASE_TAG=$PREV_RELEASE_TAG\" >> \"$GITHUB_ENV\"\n \n # Get commits between releases\n echo \"Fetching commits between $PREV_RELEASE_TAG and $RELEASE_TAG...\"\n git fetch --unshallow 2>/dev/null || git fetch --depth=1000\n \n # Get all merged PRs between the two releases (include closingIssuesReferences for attribution)\n echo \"Fetching pull requests merged between releases...\"\n PREV_PUBLISHED_AT=$(gh release view \"$PREV_RELEASE_TAG\" --json publishedAt --jq .publishedAt)\n CURR_PUBLISHED_AT=$(gh release view \"$RELEASE_TAG\" --json publishedAt --jq .publishedAt)\n gh pr list \\\n --state merged \\\n --limit 1000 \\\n --json number,title,author,labels,mergedAt,url,body,closingIssuesReferences \\\n --jq \"[.[] | select(.mergedAt >= \\\"$PREV_PUBLISHED_AT\\\" and .mergedAt <= \\\"$CURR_PUBLISHED_AT\\\")]\" \\\n > /tmp/gh-aw/agent/release-data/pull_requests.json\n \n PR_COUNT=$(jq length \"/tmp/gh-aw/agent/release-data/pull_requests.json\")\n echo \"ā Fetched $PR_COUNT pull requests\"\nfi\n\n# Build closing references index from GitHub-native closingIssuesReferences\n# Maps each closed issue number -> list of PR numbers that directly close it\necho \"Building closing references index from GitHub-native PR links...\"\n# Use a nested reduce so the outer body always returns the accumulator,\n# even when closingIssuesReferences is empty (avoids jq setting acc to null).\njq '\n reduce .[] as $pr (\n {};\n reduce ($pr.closingIssuesReferences // [])[] as $issue (\n .;\n ($issue.number | tostring) as $key |\n .[$key] = (.[$key] // []) + [$pr.number]\n )\n )\n' /tmp/gh-aw/agent/release-data/pull_requests.json \\\n > /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json 2>/dev/null \\\n || echo \"{}\" > /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json\n# Also expose to community-data dir so shared attribution strategy can reference it\ncp /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json /tmp/gh-aw/agent/community-data/closing_refs_by_issue.json\ncp /tmp/gh-aw/agent/release-data/pull_requests.json /tmp/gh-aw/agent/community-data/pull_requests.json\n\nDIRECT_CLOSE_COUNT=$(jq 'keys | length' /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json)\necho \"ā Found $DIRECT_CLOSE_COUNT issues with GitHub-native closing PR references\"\n\n# Find community issues closed during this release window (candidates for attribution review)\nif [ -n \"$PREV_PUBLISHED_AT\" ]; then\n jq --arg prev \"$PREV_PUBLISHED_AT\" --arg curr \"$CURR_PUBLISHED_AT\" \\\n '[.[] | select(.closedAt != null and .closedAt >= $prev and .closedAt <= $curr)]' \\\n /tmp/gh-aw/agent/community-data/community_issues.json \\\n > /tmp/gh-aw/agent/release-data/community_issues_closed_in_window.json 2>/dev/null \\\n || echo \"[]\" > /tmp/gh-aw/agent/release-data/community_issues_closed_in_window.json\n \n CLOSED_IN_WINDOW=$(jq length /tmp/gh-aw/agent/release-data/community_issues_closed_in_window.json)\n echo \"ā Found $CLOSED_IN_WINDOW community issues closed in this release window\"\nelse\n echo \"[]\" > /tmp/gh-aw/agent/release-data/community_issues_closed_in_window.json\nfi\n\n# Get the CHANGELOG.md content around this version\nif [ -f \"CHANGELOG.md\" ]; then\n cp CHANGELOG.md /tmp/gh-aw/agent/release-data/CHANGELOG.md\n echo \"ā Copied CHANGELOG.md for reference\"\nfi\n\n# List documentation files for linking\nfind docs -type f -name \"*.md\" 2>/dev/null > /tmp/gh-aw/agent/release-data/docs_files.txt || echo \"No docs directory found\"\n\necho \"ā Setup complete.\"\necho \" Release data: /tmp/gh-aw/agent/release-data/ (current_release.json, pull_requests.json,\"\necho \" closing_refs_by_issue.json, community_issues_closed_in_window.json,\"\necho \" CHANGELOG.md (if exists), docs_files.txt)\"\necho \" Community data: /tmp/gh-aw/agent/community-data/ (community_issues.json,\"\necho \" closing_refs_by_issue.json, pull_requests.json)\"\n"
+ run: "set -e\nmkdir -p /tmp/gh-aw/agent/release-data\nmkdir -p /tmp/gh-aw/agent/community-data\n# Copy community issues from the agent/community-data path (written by community-attribution import step)\ncp /tmp/gh-aw/agent/community-data/community_issues.json /tmp/gh-aw/agent/community-data/community_issues.json 2>/dev/null || echo \"[]\" > /tmp/gh-aw/agent/community-data/community_issues.json\n\n# Use the release ID and tag from the release job\necho \"Release ID from release job: $RELEASE_ID\"\necho \"Release tag from release job: $RELEASE_TAG\"\n\necho \"Processing release: $RELEASE_TAG\"\necho \"RELEASE_TAG=$RELEASE_TAG\" >> \"$GITHUB_ENV\"\n\n# Get the current release information\n# Use release ID to fetch release data\ngh api \"/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID\" > /tmp/gh-aw/agent/release-data/current_release.json\necho \"ā Fetched current release information\"\n\n# Get the previous release to determine the range\nPREV_RELEASE_TAG=$(gh release list --limit 2 --json tagName --jq '.[1].tagName // empty')\n\nif [ -z \"$PREV_RELEASE_TAG\" ]; then\n echo \"No previous release found. This appears to be the first release.\"\n echo \"PREV_RELEASE_TAG=\" >> \"$GITHUB_ENV\"\n touch /tmp/gh-aw/agent/release-data/pull_requests.json\n echo \"[]\" > /tmp/gh-aw/agent/release-data/pull_requests.json\nelse\n echo \"Previous release: $PREV_RELEASE_TAG\"\n echo \"PREV_RELEASE_TAG=$PREV_RELEASE_TAG\" >> \"$GITHUB_ENV\"\n \n # Get commits between releases\n echo \"Fetching commits between $PREV_RELEASE_TAG and $RELEASE_TAG...\"\n git fetch --unshallow 2>/dev/null || git fetch --depth=1000\n \n # Get all merged PRs between the two releases (include closingIssuesReferences for attribution)\n echo \"Fetching pull requests merged between releases...\"\n PREV_PUBLISHED_AT=$(gh release view \"$PREV_RELEASE_TAG\" --json publishedAt --jq .publishedAt)\n CURR_PUBLISHED_AT=$(gh release view \"$RELEASE_TAG\" --json publishedAt --jq .publishedAt)\n gh pr list \\\n --state merged \\\n --limit 1000 \\\n --json number,title,author,labels,mergedAt,url,body,closingIssuesReferences \\\n --jq \"[.[] | select(.mergedAt >= \\\"$PREV_PUBLISHED_AT\\\" and .mergedAt <= \\\"$CURR_PUBLISHED_AT\\\")]\" \\\n > /tmp/gh-aw/agent/release-data/pull_requests.json\n \n PR_COUNT=$(jq length \"/tmp/gh-aw/agent/release-data/pull_requests.json\")\n echo \"ā Fetched $PR_COUNT pull requests\"\nfi\n\n# Build closing references index from GitHub-native closingIssuesReferences\n# Maps each closed issue number -> list of PR numbers that directly close it\necho \"Building closing references index from GitHub-native PR links...\"\n# Use a nested reduce so the outer body always returns the accumulator,\n# even when closingIssuesReferences is empty (avoids jq setting acc to null).\njq '\n reduce .[] as $pr (\n {};\n reduce ($pr.closingIssuesReferences // [])[] as $issue (\n .;\n ($issue.number | tostring) as $key |\n .[$key] = (.[$key] // []) + [$pr.number]\n )\n )\n' /tmp/gh-aw/agent/release-data/pull_requests.json \\\n > /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json 2>/dev/null \\\n || echo \"{}\" > /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json\n# Also expose to community-data dir so shared attribution strategy can reference it\ncp /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json /tmp/gh-aw/agent/community-data/closing_refs_by_issue.json\ncp /tmp/gh-aw/agent/release-data/pull_requests.json /tmp/gh-aw/agent/community-data/pull_requests.json\n\nDIRECT_CLOSE_COUNT=$(jq 'keys | length' /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json)\necho \"ā Found $DIRECT_CLOSE_COUNT issues with GitHub-native closing PR references\"\n\n# Find community issues closed during this release window (candidates for attribution review)\nif [ -n \"$PREV_PUBLISHED_AT\" ]; then\n jq --arg prev \"$PREV_PUBLISHED_AT\" --arg curr \"$CURR_PUBLISHED_AT\" \\\n '[.[] | select(.closedAt != null and .closedAt >= $prev and .closedAt <= $curr)]' \\\n /tmp/gh-aw/agent/community-data/community_issues.json \\\n > /tmp/gh-aw/agent/release-data/community_issues_closed_in_window.json 2>/dev/null \\\n || echo \"[]\" > /tmp/gh-aw/agent/release-data/community_issues_closed_in_window.json\n \n CLOSED_IN_WINDOW=$(jq length /tmp/gh-aw/agent/release-data/community_issues_closed_in_window.json)\n echo \"ā Found $CLOSED_IN_WINDOW community issues closed in this release window\"\nelse\n echo \"[]\" > /tmp/gh-aw/agent/release-data/community_issues_closed_in_window.json\nfi\n\n# Get the CHANGELOG.md content around this version\nif [ -f \"CHANGELOG.md\" ]; then\n cp CHANGELOG.md /tmp/gh-aw/agent/release-data/CHANGELOG.md\n echo \"ā Copied CHANGELOG.md for reference\"\nfi\n\n# List documentation files for linking\nfind docs -type f -name \"*.md\" 2>/dev/null > /tmp/gh-aw/agent/release-data/docs_files.txt || echo \"No docs directory found\"\n\necho \"ā Setup complete.\"\necho \" Release data: /tmp/gh-aw/agent/release-data/ (current_release.json, pull_requests.json,\"\necho \" closing_refs_by_issue.json, community_issues_closed_in_window.json,\"\necho \" CHANGELOG.md (if exists), docs_files.txt)\"\necho \" Community data: /tmp/gh-aw/agent/community-data/ (community_issues.json,\"\necho \" closing_refs_by_issue.json, pull_requests.json)\""
- name: Configure Git credentials
env:
diff --git a/.github/workflows/repository-quality-improver.lock.yml b/.github/workflows/repository-quality-improver.lock.yml
index 02b41ff9441..f100c388288 100644
--- a/.github/workflows/repository-quality-improver.lock.yml
+++ b/.github/workflows/repository-quality-improver.lock.yml
@@ -493,7 +493,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Collect quality metrics
- run: "mkdir -p /tmp/gh-aw/agent\n{\n echo \"## Focus Area History\"\n if [ -f /tmp/gh-aw/cache-memory-focus-areas/history.json ]; then\n cat /tmp/gh-aw/cache-memory-focus-areas/history.json\n else\n echo '{\"runs\":[],\"recent_areas\":[],\"statistics\":{\"total_runs\":0,\"custom_rate\":0,\"reuse_rate\":0,\"unique_areas_explored\":0}}'\n fi\n\n echo \"\"\n echo \"## Code Metrics\"\n echo \"### Largest Go source files (top 20)\"\n find . -type f -name \"*.go\" ! -name \"*_test.go\" ! -path \"./.git/*\" | xargs wc -l 2>/dev/null | sort -rn | head -21 | tail -20\n\n echo \"### Test ratio\"\n TEST_LOC=$(find . -type f -name \"*_test.go\" ! -path \"./.git/*\" | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}')\n SRC_LOC=$(find . -type f -name \"*.go\" ! -name \"*_test.go\" ! -path \"./.git/*\" | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}')\n echo \"Test LOC: $TEST_LOC | Source LOC: $SRC_LOC\"\n\n echo \"### Directory file counts\"\n for dir in cmd pkg docs .github; do\n if [ -d \"$dir\" ]; then\n echo \"$dir: $(find \"$dir\" -type f | wc -l) files\"\n fi\n done\n\n echo \"### TODO/FIXME count\"\n grep -r \"TODO\\|FIXME\" --include=\"*.go\" --include=\"*.cjs\" . 2>/dev/null | wc -l\n\n echo \"### README size\"\n wc -l README.md 2>/dev/null || echo \"No README.md\"\n} > /tmp/gh-aw/agent/analysis-context.md\necho \"ā
Quality metrics collected ā /tmp/gh-aw/agent/analysis-context.md\"\n"
+ run: "mkdir -p /tmp/gh-aw/agent\n{\n echo \"## Focus Area History\"\n if [ -f /tmp/gh-aw/cache-memory-focus-areas/history.json ]; then\n cat /tmp/gh-aw/cache-memory-focus-areas/history.json\n else\n echo '{\"runs\":[],\"recent_areas\":[],\"statistics\":{\"total_runs\":0,\"custom_rate\":0,\"reuse_rate\":0,\"unique_areas_explored\":0}}'\n fi\n\n echo \"\"\n echo \"## Code Metrics\"\n echo \"### Largest Go source files (top 20)\"\n find . -type f -name \"*.go\" ! -name \"*_test.go\" ! -path \"./.git/*\" | xargs wc -l 2>/dev/null | sort -rn | head -21 | tail -20\n\n echo \"### Test ratio\"\n TEST_LOC=$(find . -type f -name \"*_test.go\" ! -path \"./.git/*\" | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}')\n SRC_LOC=$(find . -type f -name \"*.go\" ! -name \"*_test.go\" ! -path \"./.git/*\" | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}')\n echo \"Test LOC: $TEST_LOC | Source LOC: $SRC_LOC\"\n\n echo \"### Directory file counts\"\n for dir in cmd pkg docs .github; do\n if [ -d \"$dir\" ]; then\n echo \"$dir: $(find \"$dir\" -type f | wc -l) files\"\n fi\n done\n\n echo \"### TODO/FIXME count\"\n grep -r \"TODO\\|FIXME\" --include=\"*.go\" --include=\"*.cjs\" . 2>/dev/null | wc -l\n\n echo \"### README size\"\n wc -l README.md 2>/dev/null || echo \"No README.md\"\n} > /tmp/gh-aw/agent/analysis-context.md\necho \"ā
Quality metrics collected ā /tmp/gh-aw/agent/analysis-context.md\""
# Cache memory file share configuration from frontmatter processed below
- name: Create cache-memory directory (focus-areas)
diff --git a/.github/workflows/schema-consistency-checker.lock.yml b/.github/workflows/schema-consistency-checker.lock.yml
index 6b5c2951ca7..30e66705b4f 100644
--- a/.github/workflows/schema-consistency-checker.lock.yml
+++ b/.github/workflows/schema-consistency-checker.lock.yml
@@ -569,7 +569,7 @@ jobs:
GH_AW_SKILL_DIR: ".pi/skills"
run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh"
- name: Precompute schema analysis data
- run: "set -e\nmkdir -p /tmp/gh-aw/agent\n\necho \"=== Extracting schema fields ===\"\n\n# 1. All top-level fields in the main JSON schema\nSCHEMA_FIELDS=$(jq -r '.properties | keys[]' pkg/parser/schemas/main_workflow_schema.json 2>/dev/null | sort -u || echo \"\")\n\n# 2. yaml-tagged struct fields in pkg/parser/*.go\nPARSER_YAML_FIELDS=$(grep -rh 'yaml:\"' pkg/parser/*.go 2>/dev/null \\\n | grep -o 'yaml:\"[^\"]*\"' \\\n | sed 's/yaml:\"//;s/\"//' \\\n | sed 's/,omitempty//' \\\n | sed 's/,.*$//' \\\n | grep -v '^-$' \\\n | grep -v '^$' \\\n | sort -u || echo \"\")\n\n# 3. yaml-tagged struct fields in pkg/workflow/*.go\nWORKFLOW_YAML_FIELDS=$(grep -rh 'yaml:\"' pkg/workflow/*.go 2>/dev/null \\\n | grep -o 'yaml:\"[^\"]*\"' \\\n | sed 's/yaml:\"//;s/\"//' \\\n | sed 's/,omitempty//' \\\n | sed 's/,.*$//' \\\n | grep -v '^-$' \\\n | grep -v '^$' \\\n | sort -u || echo \"\")\n\n# 4. Top-level frontmatter keys actually used in workflow .md files\nUSED_FIELDS=$(grep -rh '^[a-z][a-z0-9_-]*:' .github/workflows/*.md 2>/dev/null \\\n | sed 's/:.*//' \\\n | grep -v '^#' \\\n | sort -u || echo \"\")\n\n# 5. Schema field types for all top-level fields\nFIELD_TYPES=$(jq -r '.properties | to_entries[] |\n \"\\(.key): \\(.value.type // (.value.anyOf // .value.oneOf // [] | map(.type // \"complex\") | unique | join(\"|\")) // \"complex\")\"' \\\n pkg/parser/schemas/main_workflow_schema.json 2>/dev/null | sort || echo \"\")\n\n# 6. Fields in schema but absent as yaml tags in parser structs\nIN_SCHEMA_NOT_PARSER=$(comm -23 \\\n <(echo \"$SCHEMA_FIELDS\") \\\n <(echo \"$PARSER_YAML_FIELDS\" | sort -u) 2>/dev/null || echo \"\")\n\n# 7. yaml tags in parser structs absent from schema\nIN_PARSER_NOT_SCHEMA=$(comm -23 \\\n <(echo \"$PARSER_YAML_FIELDS\" | sort -u) \\\n <(echo \"$SCHEMA_FIELDS\") 2>/dev/null || echo \"\")\n\n# 8. Fields in schema but absent from workflow compiler structs\nIN_SCHEMA_NOT_WORKFLOW=$(comm -23 \\\n <(echo \"$SCHEMA_FIELDS\") \\\n <(echo \"$WORKFLOW_YAML_FIELDS\" | sort -u) 2>/dev/null || echo \"\")\n\n# 9. Fields used in actual workflow .md files but not in schema\nIN_USED_NOT_SCHEMA=$(comm -23 \\\n <(echo \"$USED_FIELDS\" | sort -u) \\\n <(echo \"$SCHEMA_FIELDS\") 2>/dev/null || echo \"\")\n\n# Write JSON output\njq -n \\\n --arg generated_at \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n --arg schema_fields \"$SCHEMA_FIELDS\" \\\n --arg parser_yaml_fields \"$PARSER_YAML_FIELDS\" \\\n --arg workflow_yaml_fields \"$WORKFLOW_YAML_FIELDS\" \\\n --arg used_in_workflows \"$USED_FIELDS\" \\\n --arg field_types \"$FIELD_TYPES\" \\\n --arg in_schema_not_parser \"$IN_SCHEMA_NOT_PARSER\" \\\n --arg in_parser_not_schema \"$IN_PARSER_NOT_SCHEMA\" \\\n --arg in_schema_not_workflow \"$IN_SCHEMA_NOT_WORKFLOW\" \\\n --arg in_used_not_schema \"$IN_USED_NOT_SCHEMA\" \\\n '{\n generated_at: $generated_at,\n schema_fields: ($schema_fields | split(\"\\n\") | map(select(. != \"\"))),\n parser_yaml_fields: ($parser_yaml_fields | split(\"\\n\") | map(select(. != \"\"))),\n workflow_yaml_fields: ($workflow_yaml_fields | split(\"\\n\") | map(select(. != \"\"))),\n used_in_workflows: ($used_in_workflows | split(\"\\n\") | map(select(. != \"\"))),\n field_types: ($field_types | split(\"\\n\") | map(select(. != \"\"))),\n field_gaps: {\n in_schema_not_parser: ($in_schema_not_parser | split(\"\\n\") | map(select(. != \"\"))),\n in_parser_not_schema: ($in_parser_not_schema | split(\"\\n\") | map(select(. != \"\"))),\n in_schema_not_workflow: ($in_schema_not_workflow | split(\"\\n\") | map(select(. != \"\"))),\n in_used_not_schema: ($in_used_not_schema | split(\"\\n\") | map(select(. != \"\")))\n }\n }' > /tmp/gh-aw/agent/schema-diff.json\n\necho \"ā Schema diff written to /tmp/gh-aw/agent/schema-diff.json\"\necho \"Summary:\"\njq '{\n schema_field_count: (.schema_fields | length),\n parser_yaml_field_count: (.parser_yaml_fields | length),\n workflow_yaml_field_count: (.workflow_yaml_fields | length),\n gaps: {\n in_schema_not_parser: (.field_gaps.in_schema_not_parser | length),\n in_parser_not_schema: (.field_gaps.in_parser_not_schema | length),\n in_schema_not_workflow: (.field_gaps.in_schema_not_workflow | length),\n in_used_not_schema: (.field_gaps.in_used_not_schema | length)\n }\n}' /tmp/gh-aw/agent/schema-diff.json\n\necho \"=== AWF config source drift pre-check (gh-aw-firewall) ===\"\nAWF_SNAPSHOT_DIR=/tmp/gh-aw/cache-memory/awf-config-sources\nmkdir -p \"$AWF_SNAPSHOT_DIR\"\nAWF_CANONICAL_FETCH_DEGRADED=false\nAWF_USING_SNAPSHOT=false\nAWF_FETCH_FAILED_SOURCES=\"\"\n\nfetch_awf_source() {\n local source_path=\"$1\"\n local target_path=\"$2\"\n if ! gh api -H \"Accept: application/vnd.github.raw\" \"/repos/github/gh-aw-firewall/contents/${source_path}\" > \"$target_path\"; then\n AWF_CANONICAL_FETCH_DEGRADED=true\n AWF_FETCH_FAILED_SOURCES=\"${AWF_FETCH_FAILED_SOURCES}${source_path}\\n\"\n rm -f \"$target_path\"\n return 1\n fi\n}\n\nif [ -n \"${GH_TOKEN:-${GITHUB_TOKEN:-}}\" ]; then\n fetch_awf_source docs/awf-config.schema.json /tmp/gh-aw/agent/awf-config.schema.json || true\n fetch_awf_source src/awf-config-schema.json /tmp/gh-aw/agent/awf-config-runtime.schema.json || true\n fetch_awf_source docs/awf-config-spec.md /tmp/gh-aw/agent/awf-config-spec.md || true\nelse\n AWF_CANONICAL_FETCH_DEGRADED=true\n AWF_FETCH_FAILED_SOURCES=\"docs/awf-config.schema.json\\nsrc/awf-config-schema.json\\ndocs/awf-config-spec.md\\n\"\n echo \"ā ļø AWF canonical source fetch degraded: GH_TOKEN/GITHUB_TOKEN is not set\"\nfi\n\nfor source_path in docs/awf-config.schema.json src/awf-config-schema.json docs/awf-config-spec.md; do\n source_file=$(basename \"$source_path\")\n if [ \"$source_path\" = \"src/awf-config-schema.json\" ]; then\n target_path=/tmp/gh-aw/agent/awf-config-runtime.schema.json\n source_file=awf-config-runtime.schema.json\n else\n target_path=/tmp/gh-aw/agent/\"$source_file\"\n fi\n\n if [ ! -s \"$target_path\" ] && [ -s \"$AWF_SNAPSHOT_DIR/$source_file\" ]; then\n cp \"$AWF_SNAPSHOT_DIR/$source_file\" \"$target_path\"\n AWF_USING_SNAPSHOT=true\n echo \"ā ļø Using last-known AWF snapshot for $source_path\"\n fi\ndone\n\nif [ -s /tmp/gh-aw/agent/awf-config.schema.json ] && [ -s /tmp/gh-aw/agent/awf-config-runtime.schema.json ] && [ -s /tmp/gh-aw/agent/awf-config-spec.md ]; then\n cp /tmp/gh-aw/agent/awf-config.schema.json \"$AWF_SNAPSHOT_DIR/awf-config.schema.json\"\n cp /tmp/gh-aw/agent/awf-config-runtime.schema.json \"$AWF_SNAPSHOT_DIR/awf-config-runtime.schema.json\"\n cp /tmp/gh-aw/agent/awf-config-spec.md \"$AWF_SNAPSHOT_DIR/awf-config-spec.md\"\n\n jq -r '.properties | keys[]' /tmp/gh-aw/agent/awf-config.schema.json | sort -u \\\n > /tmp/gh-aw/agent/awf-config-top-level.txt\n jq -r '.properties | keys[]' /tmp/gh-aw/agent/awf-config-runtime.schema.json | sort -u \\\n > /tmp/gh-aw/agent/awf-config-runtime-top-level.txt\n rg --no-heading --no-filename 'apiProxy|container|sandbox|auth|network' pkg/workflow actions/setup \\\n | head -200 > /tmp/gh-aw/agent/awf-config-ghaw-refs.txt || true\n\n FAILED_SOURCES_JSON=$(printf \"%b\" \"$AWF_FETCH_FAILED_SOURCES\" | sed '/^$/d' | sort -u | jq -Rsc 'split(\"\\n\") | map(select(length > 0))')\n jq -n \\\n --arg generated_at \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n --arg spec_path \"docs/awf-config-spec.md\" \\\n --arg schema_path \"docs/awf-config.schema.json\" \\\n --arg runtime_schema_path \"src/awf-config-schema.json\" \\\n --arg top_level_count \"$(wc -l < /tmp/gh-aw/agent/awf-config-top-level.txt | tr -d ' ')\" \\\n --arg runtime_top_level_count \"$(wc -l < /tmp/gh-aw/agent/awf-config-runtime-top-level.txt | tr -d ' ')\" \\\n --arg refs_sample_count \"$(wc -l < /tmp/gh-aw/agent/awf-config-ghaw-refs.txt | tr -d ' ')\" \\\n --arg degraded \"$AWF_CANONICAL_FETCH_DEGRADED\" \\\n --arg using_snapshot \"$AWF_USING_SNAPSHOT\" \\\n --argjson failed_sources \"$FAILED_SOURCES_JSON\" \\\n '{\n generated_at: $generated_at,\n source_repo: \"github/gh-aw-firewall\",\n canonical_spec: $spec_path,\n canonical_schema: $schema_path,\n canonical_runtime_schema: $runtime_schema_path,\n top_level_property_count: ($top_level_count | tonumber),\n runtime_top_level_property_count: ($runtime_top_level_count | tonumber),\n ghaw_reference_sample_count: ($refs_sample_count | tonumber),\n degraded: ($degraded == \"true\"),\n using_snapshot: ($using_snapshot == \"true\"),\n failed_sources: $failed_sources\n }' > /tmp/gh-aw/agent/awf-config-drift.json\n if [ \"$AWF_CANONICAL_FETCH_DEGRADED\" = true ]; then\n echo \"ā ļø AWF canonical source fetch degraded; continuing in non-fatal mode\"\n else\n echo \"ā AWF config source pre-check artifacts written under /tmp/gh-aw/agent/\"\n fi\nelse\n AWF_CANONICAL_FETCH_DEGRADED=true\n FAILED_SOURCES_JSON=$(printf \"%b\" \"$AWF_FETCH_FAILED_SOURCES\" | sed '/^$/d' | sort -u | jq -Rsc 'split(\"\\n\") | map(select(length > 0))')\n jq -n \\\n --arg generated_at \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n --argjson failed_sources \"$FAILED_SOURCES_JSON\" \\\n '{\n generated_at: $generated_at,\n source_repo: \"github/gh-aw-firewall\",\n degraded: true,\n warning: \"canonical source retrieval failed; skipping destructive AWF drift actions\",\n failed_sources: $failed_sources\n }' > /tmp/gh-aw/agent/awf-config-drift.json\n echo \"ā ļø AWF canonical source fetch failed; run marked degraded (non-fatal)\"\nfi\n"
+ run: "set -e\nmkdir -p /tmp/gh-aw/agent\n\necho \"=== Extracting schema fields ===\"\n\n# 1. All top-level fields in the main JSON schema\nSCHEMA_FIELDS=$(jq -r '.properties | keys[]' pkg/parser/schemas/main_workflow_schema.json 2>/dev/null | sort -u || echo \"\")\n\n# 2. yaml-tagged struct fields in pkg/parser/*.go\nPARSER_YAML_FIELDS=$(grep -rh 'yaml:\"' pkg/parser/*.go 2>/dev/null \\\n | grep -o 'yaml:\"[^\"]*\"' \\\n | sed 's/yaml:\"//;s/\"//' \\\n | sed 's/,omitempty//' \\\n | sed 's/,.*$//' \\\n | grep -v '^-$' \\\n | grep -v '^$' \\\n | sort -u || echo \"\")\n\n# 3. yaml-tagged struct fields in pkg/workflow/*.go\nWORKFLOW_YAML_FIELDS=$(grep -rh 'yaml:\"' pkg/workflow/*.go 2>/dev/null \\\n | grep -o 'yaml:\"[^\"]*\"' \\\n | sed 's/yaml:\"//;s/\"//' \\\n | sed 's/,omitempty//' \\\n | sed 's/,.*$//' \\\n | grep -v '^-$' \\\n | grep -v '^$' \\\n | sort -u || echo \"\")\n\n# 4. Top-level frontmatter keys actually used in workflow .md files\nUSED_FIELDS=$(grep -rh '^[a-z][a-z0-9_-]*:' .github/workflows/*.md 2>/dev/null \\\n | sed 's/:.*//' \\\n | grep -v '^#' \\\n | sort -u || echo \"\")\n\n# 5. Schema field types for all top-level fields\nFIELD_TYPES=$(jq -r '.properties | to_entries[] |\n \"\\(.key): \\(.value.type // (.value.anyOf // .value.oneOf // [] | map(.type // \"complex\") | unique | join(\"|\")) // \"complex\")\"' \\\n pkg/parser/schemas/main_workflow_schema.json 2>/dev/null | sort || echo \"\")\n\n# 6. Fields in schema but absent as yaml tags in parser structs\nIN_SCHEMA_NOT_PARSER=$(comm -23 \\\n <(echo \"$SCHEMA_FIELDS\") \\\n <(echo \"$PARSER_YAML_FIELDS\" | sort -u) 2>/dev/null || echo \"\")\n\n# 7. yaml tags in parser structs absent from schema\nIN_PARSER_NOT_SCHEMA=$(comm -23 \\\n <(echo \"$PARSER_YAML_FIELDS\" | sort -u) \\\n <(echo \"$SCHEMA_FIELDS\") 2>/dev/null || echo \"\")\n\n# 8. Fields in schema but absent from workflow compiler structs\nIN_SCHEMA_NOT_WORKFLOW=$(comm -23 \\\n <(echo \"$SCHEMA_FIELDS\") \\\n <(echo \"$WORKFLOW_YAML_FIELDS\" | sort -u) 2>/dev/null || echo \"\")\n\n# 9. Fields used in actual workflow .md files but not in schema\nIN_USED_NOT_SCHEMA=$(comm -23 \\\n <(echo \"$USED_FIELDS\" | sort -u) \\\n <(echo \"$SCHEMA_FIELDS\") 2>/dev/null || echo \"\")\n\n# Write JSON output\njq -n \\\n --arg generated_at \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n --arg schema_fields \"$SCHEMA_FIELDS\" \\\n --arg parser_yaml_fields \"$PARSER_YAML_FIELDS\" \\\n --arg workflow_yaml_fields \"$WORKFLOW_YAML_FIELDS\" \\\n --arg used_in_workflows \"$USED_FIELDS\" \\\n --arg field_types \"$FIELD_TYPES\" \\\n --arg in_schema_not_parser \"$IN_SCHEMA_NOT_PARSER\" \\\n --arg in_parser_not_schema \"$IN_PARSER_NOT_SCHEMA\" \\\n --arg in_schema_not_workflow \"$IN_SCHEMA_NOT_WORKFLOW\" \\\n --arg in_used_not_schema \"$IN_USED_NOT_SCHEMA\" \\\n '{\n generated_at: $generated_at,\n schema_fields: ($schema_fields | split(\"\\n\") | map(select(. != \"\"))),\n parser_yaml_fields: ($parser_yaml_fields | split(\"\\n\") | map(select(. != \"\"))),\n workflow_yaml_fields: ($workflow_yaml_fields | split(\"\\n\") | map(select(. != \"\"))),\n used_in_workflows: ($used_in_workflows | split(\"\\n\") | map(select(. != \"\"))),\n field_types: ($field_types | split(\"\\n\") | map(select(. != \"\"))),\n field_gaps: {\n in_schema_not_parser: ($in_schema_not_parser | split(\"\\n\") | map(select(. != \"\"))),\n in_parser_not_schema: ($in_parser_not_schema | split(\"\\n\") | map(select(. != \"\"))),\n in_schema_not_workflow: ($in_schema_not_workflow | split(\"\\n\") | map(select(. != \"\"))),\n in_used_not_schema: ($in_used_not_schema | split(\"\\n\") | map(select(. != \"\")))\n }\n }' > /tmp/gh-aw/agent/schema-diff.json\n\necho \"ā Schema diff written to /tmp/gh-aw/agent/schema-diff.json\"\necho \"Summary:\"\njq '{\n schema_field_count: (.schema_fields | length),\n parser_yaml_field_count: (.parser_yaml_fields | length),\n workflow_yaml_field_count: (.workflow_yaml_fields | length),\n gaps: {\n in_schema_not_parser: (.field_gaps.in_schema_not_parser | length),\n in_parser_not_schema: (.field_gaps.in_parser_not_schema | length),\n in_schema_not_workflow: (.field_gaps.in_schema_not_workflow | length),\n in_used_not_schema: (.field_gaps.in_used_not_schema | length)\n }\n}' /tmp/gh-aw/agent/schema-diff.json\n\necho \"=== AWF config source drift pre-check (gh-aw-firewall) ===\"\nAWF_SNAPSHOT_DIR=/tmp/gh-aw/cache-memory/awf-config-sources\nmkdir -p \"$AWF_SNAPSHOT_DIR\"\nAWF_CANONICAL_FETCH_DEGRADED=false\nAWF_USING_SNAPSHOT=false\nAWF_FETCH_FAILED_SOURCES=\"\"\n\nfetch_awf_source() {\n local source_path=\"$1\"\n local target_path=\"$2\"\n if ! gh api -H \"Accept: application/vnd.github.raw\" \"/repos/github/gh-aw-firewall/contents/${source_path}\" > \"$target_path\"; then\n AWF_CANONICAL_FETCH_DEGRADED=true\n AWF_FETCH_FAILED_SOURCES=\"${AWF_FETCH_FAILED_SOURCES}${source_path}\\n\"\n rm -f \"$target_path\"\n return 1\n fi\n}\n\nif [ -n \"${GH_TOKEN:-${GITHUB_TOKEN:-}}\" ]; then\n fetch_awf_source docs/awf-config.schema.json /tmp/gh-aw/agent/awf-config.schema.json || true\n fetch_awf_source src/awf-config-schema.json /tmp/gh-aw/agent/awf-config-runtime.schema.json || true\n fetch_awf_source docs/awf-config-spec.md /tmp/gh-aw/agent/awf-config-spec.md || true\nelse\n AWF_CANONICAL_FETCH_DEGRADED=true\n AWF_FETCH_FAILED_SOURCES=\"docs/awf-config.schema.json\\nsrc/awf-config-schema.json\\ndocs/awf-config-spec.md\\n\"\n echo \"ā ļø AWF canonical source fetch degraded: GH_TOKEN/GITHUB_TOKEN is not set\"\nfi\n\nfor source_path in docs/awf-config.schema.json src/awf-config-schema.json docs/awf-config-spec.md; do\n source_file=$(basename \"$source_path\")\n if [ \"$source_path\" = \"src/awf-config-schema.json\" ]; then\n target_path=/tmp/gh-aw/agent/awf-config-runtime.schema.json\n source_file=awf-config-runtime.schema.json\n else\n target_path=/tmp/gh-aw/agent/\"$source_file\"\n fi\n\n if [ ! -s \"$target_path\" ] && [ -s \"$AWF_SNAPSHOT_DIR/$source_file\" ]; then\n cp \"$AWF_SNAPSHOT_DIR/$source_file\" \"$target_path\"\n AWF_USING_SNAPSHOT=true\n echo \"ā ļø Using last-known AWF snapshot for $source_path\"\n fi\ndone\n\nif [ -s /tmp/gh-aw/agent/awf-config.schema.json ] && [ -s /tmp/gh-aw/agent/awf-config-runtime.schema.json ] && [ -s /tmp/gh-aw/agent/awf-config-spec.md ]; then\n cp /tmp/gh-aw/agent/awf-config.schema.json \"$AWF_SNAPSHOT_DIR/awf-config.schema.json\"\n cp /tmp/gh-aw/agent/awf-config-runtime.schema.json \"$AWF_SNAPSHOT_DIR/awf-config-runtime.schema.json\"\n cp /tmp/gh-aw/agent/awf-config-spec.md \"$AWF_SNAPSHOT_DIR/awf-config-spec.md\"\n\n jq -r '.properties | keys[]' /tmp/gh-aw/agent/awf-config.schema.json | sort -u \\\n > /tmp/gh-aw/agent/awf-config-top-level.txt\n jq -r '.properties | keys[]' /tmp/gh-aw/agent/awf-config-runtime.schema.json | sort -u \\\n > /tmp/gh-aw/agent/awf-config-runtime-top-level.txt\n rg --no-heading --no-filename 'apiProxy|container|sandbox|auth|network' pkg/workflow actions/setup \\\n | head -200 > /tmp/gh-aw/agent/awf-config-ghaw-refs.txt || true\n\n FAILED_SOURCES_JSON=$(printf \"%b\" \"$AWF_FETCH_FAILED_SOURCES\" | sed '/^$/d' | sort -u | jq -Rsc 'split(\"\\n\") | map(select(length > 0))')\n jq -n \\\n --arg generated_at \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n --arg spec_path \"docs/awf-config-spec.md\" \\\n --arg schema_path \"docs/awf-config.schema.json\" \\\n --arg runtime_schema_path \"src/awf-config-schema.json\" \\\n --arg top_level_count \"$(wc -l < /tmp/gh-aw/agent/awf-config-top-level.txt | tr -d ' ')\" \\\n --arg runtime_top_level_count \"$(wc -l < /tmp/gh-aw/agent/awf-config-runtime-top-level.txt | tr -d ' ')\" \\\n --arg refs_sample_count \"$(wc -l < /tmp/gh-aw/agent/awf-config-ghaw-refs.txt | tr -d ' ')\" \\\n --arg degraded \"$AWF_CANONICAL_FETCH_DEGRADED\" \\\n --arg using_snapshot \"$AWF_USING_SNAPSHOT\" \\\n --argjson failed_sources \"$FAILED_SOURCES_JSON\" \\\n '{\n generated_at: $generated_at,\n source_repo: \"github/gh-aw-firewall\",\n canonical_spec: $spec_path,\n canonical_schema: $schema_path,\n canonical_runtime_schema: $runtime_schema_path,\n top_level_property_count: ($top_level_count | tonumber),\n runtime_top_level_property_count: ($runtime_top_level_count | tonumber),\n ghaw_reference_sample_count: ($refs_sample_count | tonumber),\n degraded: ($degraded == \"true\"),\n using_snapshot: ($using_snapshot == \"true\"),\n failed_sources: $failed_sources\n }' > /tmp/gh-aw/agent/awf-config-drift.json\n if [ \"$AWF_CANONICAL_FETCH_DEGRADED\" = true ]; then\n echo \"ā ļø AWF canonical source fetch degraded; continuing in non-fatal mode\"\n else\n echo \"ā AWF config source pre-check artifacts written under /tmp/gh-aw/agent/\"\n fi\nelse\n AWF_CANONICAL_FETCH_DEGRADED=true\n FAILED_SOURCES_JSON=$(printf \"%b\" \"$AWF_FETCH_FAILED_SOURCES\" | sed '/^$/d' | sort -u | jq -Rsc 'split(\"\\n\") | map(select(length > 0))')\n jq -n \\\n --arg generated_at \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n --argjson failed_sources \"$FAILED_SOURCES_JSON\" \\\n '{\n generated_at: $generated_at,\n source_repo: \"github/gh-aw-firewall\",\n degraded: true,\n warning: \"canonical source retrieval failed; skipping destructive AWF drift actions\",\n failed_sources: $failed_sources\n }' > /tmp/gh-aw/agent/awf-config-drift.json\n echo \"ā ļø AWF canonical source fetch failed; run marked degraded (non-fatal)\"\nfi"
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.24@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4
diff --git a/.github/workflows/spec-extractor.lock.yml b/.github/workflows/spec-extractor.lock.yml
index 433d4a21177..95b59a460c2 100644
--- a/.github/workflows/spec-extractor.lock.yml
+++ b/.github/workflows/spec-extractor.lock.yml
@@ -564,7 +564,7 @@ jobs:
GH_AW_SKILL_DIR: ".github/skills"
run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh"
- name: Collect package analysis data
- run: "set -e\nPACKAGES=(agentdrain cli console constants envutil fileutil gitutil logger parser repoutil semverutil sliceutil stringutil styles testutil timeutil tty types typeutil workflow)\nTOTAL=${#PACKAGES[@]}\nCACHE_DIR=/tmp/gh-aw/cache-memory/spec-extractor\nCONTEXT=/tmp/gh-aw/agent/pkg-context.md\n\n# Initialize or load rotation state\nmkdir -p \"$CACHE_DIR/extractions\"\nif [ -f \"$CACHE_DIR/rotation.json\" ]; then\n LAST_INDEX=$(python3 -c \"import json; d=json.load(open('$CACHE_DIR/rotation.json')); print(d.get('last_index', 0))\" 2>/dev/null || echo 0)\nelse\n printf '{\"last_index\":0,\"last_packages\":[],\"last_run\":\"\",\"total_packages\":%d}\\n' \"$TOTAL\" > \"$CACHE_DIR/rotation.json\"\n LAST_INDEX=0\nfi\n\n# Select next 4 packages using round-robin\nPKG0=\"${PACKAGES[$((LAST_INDEX % TOTAL))]}\"\nPKG1=\"${PACKAGES[$(((LAST_INDEX + 1) % TOTAL))]}\"\nPKG2=\"${PACKAGES[$(((LAST_INDEX + 2) % TOTAL))]}\"\nPKG3=\"${PACKAGES[$(((LAST_INDEX + 3) % TOTAL))]}\"\nNEXT_INDEX=$(((LAST_INDEX + 4) % TOTAL))\nSELECTED=(\"$PKG0\" \"$PKG1\" \"$PKG2\" \"$PKG3\")\n\necho \"Selected packages: ${SELECTED[*]} (last_index=$LAST_INDEX, next=$NEXT_INDEX)\"\n\n# Collect analysis data for each package into the context file\n{\n echo \"# Package Analysis Context\"\n echo \"\"\n echo \"**Run date**: $(date -u +%Y-%m-%d)\"\n echo \"**Rotation last_index (current)**: $LAST_INDEX\"\n echo \"**Selected packages**: ${SELECTED[*]}\"\n echo \"**Next last_index (save this after run)**: $NEXT_INDEX\"\n echo \"\"\n\n for PKG in \"${SELECTED[@]}\"; do\n echo \"---\"\n echo \"\"\n echo \"## Package: \\`$PKG\\`\"\n echo \"\"\n\n echo \"### Source Files\"\n echo '```'\n find \"pkg/$PKG\" -name '*.go' ! -name '*_test.go' -type f 2>/dev/null | sort || true\n echo '```'\n\n echo \"### Line Counts\"\n echo '```'\n wc -l pkg/\"$PKG\"/*.go 2>/dev/null || true\n echo '```'\n\n echo \"### Exported Functions\"\n echo '```'\n grep -rn \"^func [A-Z]\" \"pkg/$PKG\" --include='*.go' 2>/dev/null || true\n echo '```'\n\n echo \"### Exported Types\"\n echo '```'\n grep -rn \"^type [A-Z]\" \"pkg/$PKG\" --include='*.go' 2>/dev/null || true\n echo '```'\n\n echo \"### Exported Constants\"\n echo '```'\n grep -rn \"^const [A-Z]\" \"pkg/$PKG\" --include='*.go' 2>/dev/null || true\n echo '```'\n\n echo \"### Exported Variables\"\n echo '```'\n grep -rn \"^var [A-Z]\" \"pkg/$PKG\" --include='*.go' 2>/dev/null || true\n echo '```'\n\n echo \"### Package Doc Comments (first 30 lines per file)\"\n for f in pkg/\"$PKG\"/*.go; do\n [ -f \"$f\" ] || continue\n echo \"#### $f\"\n echo '```go'\n head -n 30 \"$f\" 2>/dev/null || true\n echo '```'\n done\n\n echo \"### Imports\"\n echo '```'\n find \"pkg/$PKG\" -name '*.go' ! -name '*_test.go' -type f | xargs grep -h \"import\" 2>/dev/null | sort -u || true\n echo '```'\n\n echo \"### Existing README.md\"\n echo '```markdown'\n cat \"pkg/$PKG/README.md\" 2>/dev/null || echo \"No existing README.md\"\n echo '```'\n\n echo \"### Recent Git History (30 days)\"\n echo '```'\n git log --oneline --since='30 days ago' -- \"pkg/$PKG\" 2>/dev/null || true\n echo '```'\n echo \"\"\n done\n} > \"$CONTEXT\"\necho \"Context file written to $CONTEXT ($(wc -l < \"$CONTEXT\") lines)\"\n"
+ run: "set -e\nPACKAGES=(agentdrain cli console constants envutil fileutil gitutil logger parser repoutil semverutil sliceutil stringutil styles testutil timeutil tty types typeutil workflow)\nTOTAL=${#PACKAGES[@]}\nCACHE_DIR=/tmp/gh-aw/cache-memory/spec-extractor\nCONTEXT=/tmp/gh-aw/agent/pkg-context.md\n\n# Initialize or load rotation state\nmkdir -p \"$CACHE_DIR/extractions\"\nif [ -f \"$CACHE_DIR/rotation.json\" ]; then\n LAST_INDEX=$(python3 -c \"import json; d=json.load(open('$CACHE_DIR/rotation.json')); print(d.get('last_index', 0))\" 2>/dev/null || echo 0)\nelse\n printf '{\"last_index\":0,\"last_packages\":[],\"last_run\":\"\",\"total_packages\":%d}\\n' \"$TOTAL\" > \"$CACHE_DIR/rotation.json\"\n LAST_INDEX=0\nfi\n\n# Select next 4 packages using round-robin\nPKG0=\"${PACKAGES[$((LAST_INDEX % TOTAL))]}\"\nPKG1=\"${PACKAGES[$(((LAST_INDEX + 1) % TOTAL))]}\"\nPKG2=\"${PACKAGES[$(((LAST_INDEX + 2) % TOTAL))]}\"\nPKG3=\"${PACKAGES[$(((LAST_INDEX + 3) % TOTAL))]}\"\nNEXT_INDEX=$(((LAST_INDEX + 4) % TOTAL))\nSELECTED=(\"$PKG0\" \"$PKG1\" \"$PKG2\" \"$PKG3\")\n\necho \"Selected packages: ${SELECTED[*]} (last_index=$LAST_INDEX, next=$NEXT_INDEX)\"\n\n# Collect analysis data for each package into the context file\n{\n echo \"# Package Analysis Context\"\n echo \"\"\n echo \"**Run date**: $(date -u +%Y-%m-%d)\"\n echo \"**Rotation last_index (current)**: $LAST_INDEX\"\n echo \"**Selected packages**: ${SELECTED[*]}\"\n echo \"**Next last_index (save this after run)**: $NEXT_INDEX\"\n echo \"\"\n\n for PKG in \"${SELECTED[@]}\"; do\n echo \"---\"\n echo \"\"\n echo \"## Package: \\`$PKG\\`\"\n echo \"\"\n\n echo \"### Source Files\"\n echo '```'\n find \"pkg/$PKG\" -name '*.go' ! -name '*_test.go' -type f 2>/dev/null | sort || true\n echo '```'\n\n echo \"### Line Counts\"\n echo '```'\n wc -l pkg/\"$PKG\"/*.go 2>/dev/null || true\n echo '```'\n\n echo \"### Exported Functions\"\n echo '```'\n grep -rn \"^func [A-Z]\" \"pkg/$PKG\" --include='*.go' 2>/dev/null || true\n echo '```'\n\n echo \"### Exported Types\"\n echo '```'\n grep -rn \"^type [A-Z]\" \"pkg/$PKG\" --include='*.go' 2>/dev/null || true\n echo '```'\n\n echo \"### Exported Constants\"\n echo '```'\n grep -rn \"^const [A-Z]\" \"pkg/$PKG\" --include='*.go' 2>/dev/null || true\n echo '```'\n\n echo \"### Exported Variables\"\n echo '```'\n grep -rn \"^var [A-Z]\" \"pkg/$PKG\" --include='*.go' 2>/dev/null || true\n echo '```'\n\n echo \"### Package Doc Comments (first 30 lines per file)\"\n for f in pkg/\"$PKG\"/*.go; do\n [ -f \"$f\" ] || continue\n echo \"#### $f\"\n echo '```go'\n head -n 30 \"$f\" 2>/dev/null || true\n echo '```'\n done\n\n echo \"### Imports\"\n echo '```'\n find \"pkg/$PKG\" -name '*.go' ! -name '*_test.go' -type f | xargs grep -h \"import\" 2>/dev/null | sort -u || true\n echo '```'\n\n echo \"### Existing README.md\"\n echo '```markdown'\n cat \"pkg/$PKG/README.md\" 2>/dev/null || echo \"No existing README.md\"\n echo '```'\n\n echo \"### Recent Git History (30 days)\"\n echo '```'\n git log --oneline --since='30 days ago' -- \"pkg/$PKG\" 2>/dev/null || true\n echo '```'\n echo \"\"\n done\n} > \"$CONTEXT\"\necho \"Context file written to $CONTEXT ($(wc -l < \"$CONTEXT\") lines)\""
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.24@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4 ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5
diff --git a/.github/workflows/stale-repo-identifier.lock.yml b/.github/workflows/stale-repo-identifier.lock.yml
index 3824e5ff292..3465903f59d 100644
--- a/.github/workflows/stale-repo-identifier.lock.yml
+++ b/.github/workflows/stale-repo-identifier.lock.yml
@@ -575,7 +575,7 @@ jobs:
NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt
ORGANIZATION: ${{ env.ORGANIZATION }}
- name: Save stale repos output
- run: |
+ run: |-
mkdir -p /tmp/gh-aw/agent/stale-repos-data
echo "$INACTIVE_REPOS" > /tmp/gh-aw/agent/stale-repos-data/inactive-repos.json
echo "Stale repositories data saved"
diff --git a/.github/workflows/static-analysis-report.lock.yml b/.github/workflows/static-analysis-report.lock.yml
index b3603ded51f..dc2512f46d8 100644
--- a/.github/workflows/static-analysis-report.lock.yml
+++ b/.github/workflows/static-analysis-report.lock.yml
@@ -541,7 +541,7 @@ jobs:
- name: Verify static analysis tools
run: "set -e\necho \"Verifying static analysis tools are available...\"\n\n# Verify zizmor\necho \"Testing zizmor...\"\ndocker run --rm ghcr.io/zizmorcore/zizmor:latest --version || echo \"Warning: zizmor version check failed\"\n\n# Verify poutine\necho \"Testing poutine...\"\ndocker run --rm ghcr.io/boostsecurityio/poutine:latest --version || echo \"Warning: poutine version check failed\"\n\n# Verify runner-guard\necho \"Testing runner-guard...\"\ndocker run --rm ghcr.io/vigilant-llc/runner-guard:latest --version || echo \"Warning: runner-guard version check failed\"\n\necho \"Static analysis tools verification complete\"\n"
- name: Run compile with security tools
- run: "set -e\necho \"Running gh aw compile with security tools to download Docker images...\"\n\n# Run compile with all security scanner flags to download Docker images\n# Store the output in a file for inspection\n\"$GITHUB_WORKSPACE/gh-aw\" compile --zizmor --poutine --actionlint --runner-guard 2>&1 | tee /tmp/gh-aw/agent/compile-output.txt\n\necho \"Compile with security tools completed\"\necho \"Output saved to /tmp/gh-aw/agent/compile-output.txt\"\n"
+ run: "set -e\necho \"Running gh aw compile with security tools to download Docker images...\"\n\n# Run compile with all security scanner flags to download Docker images\n# Store the output in a file for inspection\n\"$GITHUB_WORKSPACE/gh-aw\" compile --zizmor --poutine --actionlint --runner-guard 2>&1 | tee /tmp/gh-aw/agent/compile-output.txt\n\necho \"Compile with security tools completed\"\necho \"Output saved to /tmp/gh-aw/agent/compile-output.txt\""
# Cache memory file share configuration from frontmatter processed below
- name: Create cache-memory directory
diff --git a/.github/workflows/step-name-alignment.lock.yml b/.github/workflows/step-name-alignment.lock.yml
index 4c88a31a24d..6c89ee64f04 100644
--- a/.github/workflows/step-name-alignment.lock.yml
+++ b/.github/workflows/step-name-alignment.lock.yml
@@ -480,7 +480,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
- name: Build step alignment manifest
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\nMANIFEST_JSONL=\"/tmp/gh-aw/agent/step-alignment-input.jsonl\"\nMANIFEST_JSON=\"/tmp/gh-aw/agent/step-alignment-input.json\"\n: > \"$MANIFEST_JSONL\"\n\nwhile IFS= read -r workflow_file; do\n yq -o=json \\\n '.jobs | to_entries[] | .value.steps[]? | {\"workflow_file\": \"'\"$workflow_file\"'\", \"step_name\": (.name // \"\"), \"action_uses\": (.uses // \"\")}' \\\n \"$workflow_file\" >> \"$MANIFEST_JSONL\"\ndone < <(find .github/workflows -name \"*.lock.yml\" -type f | sort)\n\njq -s '.' \"$MANIFEST_JSONL\" > \"$MANIFEST_JSON\"\nrm -f \"$MANIFEST_JSONL\"\n\necho \"Wrote $(jq 'length' \"$MANIFEST_JSON\") step records to $MANIFEST_JSON\"\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\nMANIFEST_JSONL=\"/tmp/gh-aw/agent/step-alignment-input.jsonl\"\nMANIFEST_JSON=\"/tmp/gh-aw/agent/step-alignment-input.json\"\n: > \"$MANIFEST_JSONL\"\n\nwhile IFS= read -r workflow_file; do\n yq -o=json \\\n '.jobs | to_entries[] | .value.steps[]? | {\"workflow_file\": \"'\"$workflow_file\"'\", \"step_name\": (.name // \"\"), \"action_uses\": (.uses // \"\")}' \\\n \"$workflow_file\" >> \"$MANIFEST_JSONL\"\ndone < <(find .github/workflows -name \"*.lock.yml\" -type f | sort)\n\njq -s '.' \"$MANIFEST_JSONL\" > \"$MANIFEST_JSON\"\nrm -f \"$MANIFEST_JSONL\"\n\necho \"Wrote $(jq 'length' \"$MANIFEST_JSON\") step records to $MANIFEST_JSON\""
# Cache memory file share configuration from frontmatter processed below
- name: Create cache-memory directory
diff --git a/.github/workflows/test-quality-sentinel.lock.yml b/.github/workflows/test-quality-sentinel.lock.yml
index 0c6d1dd379d..aa3d110da05 100644
--- a/.github/workflows/test-quality-sentinel.lock.yml
+++ b/.github/workflows/test-quality-sentinel.lock.yml
@@ -574,7 +574,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
name: Pre-fetch PR data
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" \\\n --json files,additions,deletions,baseRefName,headRefName \\\n > /tmp/gh-aw/agent/pr-meta.json\n\n# List of changed test files\ngh pr diff \"$PR_NUMBER\" \\\n --name-only | grep -E '(_test\\.go|\\.test\\.cjs|\\.test\\.js)$' \\\n > /tmp/gh-aw/agent/test-files.txt || true\n\n# Diff for test files only; capped at 40 KB to control cache token costs\nif [ -s /tmp/gh-aw/agent/test-files.txt ]; then\n # shellcheck disable=SC2046\n gh pr diff \"$PR_NUMBER\" \\\n -- $(tr '\\n' ' ' < /tmp/gh-aw/agent/test-files.txt) \\\n | head -c 40000 \\\n > /tmp/gh-aw/agent/test-diff.txt 2>/dev/null || true\nelse\n touch /tmp/gh-aw/agent/test-diff.txt\nfi\n\ngit diff \"$EXPR_GITHUB_EVENT_PULL_REQUEST_BASE_SHA...HEAD\" --numstat \\\n > /tmp/gh-aw/agent/diff-numstat.txt 2>/dev/null || true\n\n# Extract new/modified test function signatures from the diff\nif [ -s /tmp/gh-aw/agent/test-diff.txt ]; then\n grep -E \"^\\+func Test\" /tmp/gh-aw/agent/test-diff.txt \\\n > /tmp/gh-aw/agent/go-new-test-funcs.txt || true\n grep -E \"^\\+(it|test|describe)\\(\" /tmp/gh-aw/agent/test-diff.txt \\\n > /tmp/gh-aw/agent/js-new-test-funcs.txt || true\n # Check for new Go test files missing mandatory build tags\n git diff \"$EXPR_GITHUB_EVENT_PULL_REQUEST_BASE_SHA...HEAD\" \\\n --diff-filter=A --name-only 2>/dev/null \\\n | grep '_test\\.go$' | while read -r f; do\n if ! head -1 \"$f\" | grep -qE '^//go:build'; then\n echo \"MISSING BUILD TAG: $f\"\n fi\n done > /tmp/gh-aw/agent/missing-build-tags.txt || true\n # Go test structural stats (assertions, error checks, table-driven, forbidden mocks)\n awk '\n /^\\+func Test/ {\n if (test_name) print test_name, \"assertions=\" assertions, \"errors=\" errors, \"table_driven=\" table_driven, \"forbidden_mocks=\" forbidden_mocks\n match($0, /func (Test[^(]+)/, arr); test_name=arr[1]; assertions=0; errors=0; table_driven=0; forbidden_mocks=0\n }\n test_name && /^\\+.*(assert\\.|require\\.)/ { assertions++ }\n test_name && /^\\+.*t\\.(Error|Errorf|Fatal|Fatalf)\\(/ { assertions++; errors++ }\n test_name && /^\\+.*(assert\\.Error|require\\.Error|assert\\.NoError|require\\.NoError)/ { errors++ }\n test_name && /^\\+.*t\\.Run\\(/ { table_driven++ }\n test_name && /^\\+.*(gomock\\.|testify\\/mock|\\.EXPECT\\(\\)|\\.On\\(|\\.Return\\()/ { forbidden_mocks++ }\n test_name && /^\\+\\}$/ { print test_name, \"assertions=\" assertions, \"errors=\" errors, \"table_driven=\" table_driven, \"forbidden_mocks=\" forbidden_mocks; test_name=\"\" }\n END { if (test_name) print test_name, \"assertions=\" assertions, \"errors=\" errors, \"table_driven=\" table_driven, \"forbidden_mocks=\" forbidden_mocks }\n ' /tmp/gh-aw/agent/test-diff.txt > /tmp/gh-aw/agent/go-test-stats.txt || true\n # JS test structural stats (assertions, error matchers, vi.* mocks)\n awk '\n /^\\+(it|test)\\(/ {\n if (test_name) print test_name, \"assertions=\" assertions, \"errors=\" errors, \"mocks=\" mocks\n match($0, /(it|test)\\([\"\\047]([^\"\\047]+)/, arr); test_name=arr[2]; assertions=0; errors=0; mocks=0\n }\n test_name && /^\\+.*expect\\(/ { assertions++ }\n test_name && /^\\+.*(\\.toThrow|\\.rejects|\\.toThrowError)/ { errors++ }\n test_name && /^\\+.*(vi\\.mock|vi\\.spyOn|vi\\.fn)/ { mocks++ }\n test_name && /^\\+\\}\\)/ { print test_name, \"assertions=\" assertions, \"errors=\" errors, \"mocks=\" mocks; test_name=\"\" }\n END { if (test_name) print test_name, \"assertions=\" assertions, \"errors=\" errors, \"mocks=\" mocks }\n ' /tmp/gh-aw/agent/test-diff.txt > /tmp/gh-aw/agent/js-test-stats.txt || true\nelse\n touch /tmp/gh-aw/agent/go-new-test-funcs.txt \\\n /tmp/gh-aw/agent/js-new-test-funcs.txt \\\n /tmp/gh-aw/agent/missing-build-tags.txt \\\n /tmp/gh-aw/agent/go-test-stats.txt \\\n /tmp/gh-aw/agent/js-test-stats.txt\nfi\n\necho \"Pre-fetched $(grep -c . /tmp/gh-aw/agent/test-files.txt || echo 0) test files\"\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\n# PR metadata\ngh pr view \"$PR_NUMBER\" \\\n --json files,additions,deletions,baseRefName,headRefName \\\n > /tmp/gh-aw/agent/pr-meta.json\n\n# List of changed test files\ngh pr diff \"$PR_NUMBER\" \\\n --name-only | grep -E '(_test\\.go|\\.test\\.cjs|\\.test\\.js)$' \\\n > /tmp/gh-aw/agent/test-files.txt || true\n\n# Diff for test files only; capped at 40 KB to control cache token costs\nif [ -s /tmp/gh-aw/agent/test-files.txt ]; then\n # shellcheck disable=SC2046\n gh pr diff \"$PR_NUMBER\" \\\n -- $(tr '\\n' ' ' < /tmp/gh-aw/agent/test-files.txt) \\\n | head -c 40000 \\\n > /tmp/gh-aw/agent/test-diff.txt 2>/dev/null || true\nelse\n touch /tmp/gh-aw/agent/test-diff.txt\nfi\n\ngit diff \"$EXPR_GITHUB_EVENT_PULL_REQUEST_BASE_SHA...HEAD\" --numstat \\\n > /tmp/gh-aw/agent/diff-numstat.txt 2>/dev/null || true\n\n# Extract new/modified test function signatures from the diff\nif [ -s /tmp/gh-aw/agent/test-diff.txt ]; then\n grep -E \"^\\+func Test\" /tmp/gh-aw/agent/test-diff.txt \\\n > /tmp/gh-aw/agent/go-new-test-funcs.txt || true\n grep -E \"^\\+(it|test|describe)\\(\" /tmp/gh-aw/agent/test-diff.txt \\\n > /tmp/gh-aw/agent/js-new-test-funcs.txt || true\n # Check for new Go test files missing mandatory build tags\n git diff \"$EXPR_GITHUB_EVENT_PULL_REQUEST_BASE_SHA...HEAD\" \\\n --diff-filter=A --name-only 2>/dev/null \\\n | grep '_test\\.go$' | while read -r f; do\n if ! head -1 \"$f\" | grep -qE '^//go:build'; then\n echo \"MISSING BUILD TAG: $f\"\n fi\n done > /tmp/gh-aw/agent/missing-build-tags.txt || true\n # Go test structural stats (assertions, error checks, table-driven, forbidden mocks)\n awk '\n /^\\+func Test/ {\n if (test_name) print test_name, \"assertions=\" assertions, \"errors=\" errors, \"table_driven=\" table_driven, \"forbidden_mocks=\" forbidden_mocks\n match($0, /func (Test[^(]+)/, arr); test_name=arr[1]; assertions=0; errors=0; table_driven=0; forbidden_mocks=0\n }\n test_name && /^\\+.*(assert\\.|require\\.)/ { assertions++ }\n test_name && /^\\+.*t\\.(Error|Errorf|Fatal|Fatalf)\\(/ { assertions++; errors++ }\n test_name && /^\\+.*(assert\\.Error|require\\.Error|assert\\.NoError|require\\.NoError)/ { errors++ }\n test_name && /^\\+.*t\\.Run\\(/ { table_driven++ }\n test_name && /^\\+.*(gomock\\.|testify\\/mock|\\.EXPECT\\(\\)|\\.On\\(|\\.Return\\()/ { forbidden_mocks++ }\n test_name && /^\\+\\}$/ { print test_name, \"assertions=\" assertions, \"errors=\" errors, \"table_driven=\" table_driven, \"forbidden_mocks=\" forbidden_mocks; test_name=\"\" }\n END { if (test_name) print test_name, \"assertions=\" assertions, \"errors=\" errors, \"table_driven=\" table_driven, \"forbidden_mocks=\" forbidden_mocks }\n ' /tmp/gh-aw/agent/test-diff.txt > /tmp/gh-aw/agent/go-test-stats.txt || true\n # JS test structural stats (assertions, error matchers, vi.* mocks)\n awk '\n /^\\+(it|test)\\(/ {\n if (test_name) print test_name, \"assertions=\" assertions, \"errors=\" errors, \"mocks=\" mocks\n match($0, /(it|test)\\([\"\\047]([^\"\\047]+)/, arr); test_name=arr[2]; assertions=0; errors=0; mocks=0\n }\n test_name && /^\\+.*expect\\(/ { assertions++ }\n test_name && /^\\+.*(\\.toThrow|\\.rejects|\\.toThrowError)/ { errors++ }\n test_name && /^\\+.*(vi\\.mock|vi\\.spyOn|vi\\.fn)/ { mocks++ }\n test_name && /^\\+\\}\\)/ { print test_name, \"assertions=\" assertions, \"errors=\" errors, \"mocks=\" mocks; test_name=\"\" }\n END { if (test_name) print test_name, \"assertions=\" assertions, \"errors=\" errors, \"mocks=\" mocks }\n ' /tmp/gh-aw/agent/test-diff.txt > /tmp/gh-aw/agent/js-test-stats.txt || true\nelse\n touch /tmp/gh-aw/agent/go-new-test-funcs.txt \\\n /tmp/gh-aw/agent/js-new-test-funcs.txt \\\n /tmp/gh-aw/agent/missing-build-tags.txt \\\n /tmp/gh-aw/agent/go-test-stats.txt \\\n /tmp/gh-aw/agent/js-test-stats.txt\nfi\n\necho \"Pre-fetched $(grep -c . /tmp/gh-aw/agent/test-files.txt || echo 0) test files\""
- name: Configure Git credentials
env:
diff --git a/.github/workflows/uk-ai-operational-resilience.lock.yml b/.github/workflows/uk-ai-operational-resilience.lock.yml
index 83c3409c359..4b44a0f0085 100644
--- a/.github/workflows/uk-ai-operational-resilience.lock.yml
+++ b/.github/workflows/uk-ai-operational-resilience.lock.yml
@@ -544,7 +544,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LOOKBACK_DAYS: ${{ github.event.inputs.lookback_days || '7' }}
name: Pre-compute recent changes governance context
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\nREPO=\"${GITHUB_REPOSITORY}\"\nDAYS=\"${LOOKBACK_DAYS}\"\nif ! [[ \"$DAYS\" =~ ^[0-9]+$ ]]; then DAYS=7; fi\nif [ \"$DAYS\" -lt 1 ]; then DAYS=1; fi\nif [ \"$DAYS\" -gt 30 ]; then DAYS=30; fi\n\nSINCE=\"$(date -u -d \"-${DAYS} days\" +%Y-%m-%dT%H:%M:%SZ)\"\necho \"$SINCE\" >/tmp/gh-aw/agent/since.txt\n\ngh api --paginate \"repos/${REPO}/commits?since=${SINCE}&per_page=100\" | jq -s 'add // []' > /tmp/gh-aw/agent/recent-commits.json\ngh api --paginate \"repos/${REPO}/issues?state=open&labels=security&per_page=100\" | jq -s 'add // []' > /tmp/gh-aw/agent/open-security-issues.json\ngh api --paginate \"repos/${REPO}/code-scanning/alerts?state=open&per_page=100\" | jq -s 'add // []' > /tmp/gh-aw/agent/open-code-scanning-alerts.json || echo \"[]\" >/tmp/gh-aw/agent/open-code-scanning-alerts.json\ngh api --paginate \"repos/${REPO}/secret-scanning/alerts?state=open&per_page=100\" | jq -s 'add // []' > /tmp/gh-aw/agent/open-secret-scanning-alerts.json || echo \"[]\" >/tmp/gh-aw/agent/open-secret-scanning-alerts.json\n\njq -r '\n [.[].commit.message]\n | map(select(type==\"string\"))\n | map(\n if test(\"security|vuln|cve|patch|auth|secret|token|permissions|hardening\"; \"i\")\n then .\n else empty\n end\n )' /tmp/gh-aw/agent/recent-commits.json > /tmp/gh-aw/agent/security-signal-commits.json\n\n{\n echo \"## UK AI Governance Pre-compute\"\n echo \"- Repository: ${REPO}\"\n echo \"- Lookback days: ${DAYS}\"\n echo \"- Since: ${SINCE}\"\n echo \"- Commits in window: $(jq 'length' /tmp/gh-aw/agent/recent-commits.json)\"\n echo \"- Security-signal commits: $(jq 'length' /tmp/gh-aw/agent/security-signal-commits.json)\"\n echo \"- Open security issues: $(jq 'length' /tmp/gh-aw/agent/open-security-issues.json)\"\n echo \"- Open code scanning alerts: $(jq 'length' /tmp/gh-aw/agent/open-code-scanning-alerts.json)\"\n echo \"- Open secret scanning alerts: $(jq 'length' /tmp/gh-aw/agent/open-secret-scanning-alerts.json)\"\n} > /tmp/gh-aw/agent/uk-ai-governance-context.md\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n\nREPO=\"${GITHUB_REPOSITORY}\"\nDAYS=\"${LOOKBACK_DAYS}\"\nif ! [[ \"$DAYS\" =~ ^[0-9]+$ ]]; then DAYS=7; fi\nif [ \"$DAYS\" -lt 1 ]; then DAYS=1; fi\nif [ \"$DAYS\" -gt 30 ]; then DAYS=30; fi\n\nSINCE=\"$(date -u -d \"-${DAYS} days\" +%Y-%m-%dT%H:%M:%SZ)\"\necho \"$SINCE\" >/tmp/gh-aw/agent/since.txt\n\ngh api --paginate \"repos/${REPO}/commits?since=${SINCE}&per_page=100\" | jq -s 'add // []' > /tmp/gh-aw/agent/recent-commits.json\ngh api --paginate \"repos/${REPO}/issues?state=open&labels=security&per_page=100\" | jq -s 'add // []' > /tmp/gh-aw/agent/open-security-issues.json\ngh api --paginate \"repos/${REPO}/code-scanning/alerts?state=open&per_page=100\" | jq -s 'add // []' > /tmp/gh-aw/agent/open-code-scanning-alerts.json || echo \"[]\" >/tmp/gh-aw/agent/open-code-scanning-alerts.json\ngh api --paginate \"repos/${REPO}/secret-scanning/alerts?state=open&per_page=100\" | jq -s 'add // []' > /tmp/gh-aw/agent/open-secret-scanning-alerts.json || echo \"[]\" >/tmp/gh-aw/agent/open-secret-scanning-alerts.json\n\njq -r '\n [.[].commit.message]\n | map(select(type==\"string\"))\n | map(\n if test(\"security|vuln|cve|patch|auth|secret|token|permissions|hardening\"; \"i\")\n then .\n else empty\n end\n )' /tmp/gh-aw/agent/recent-commits.json > /tmp/gh-aw/agent/security-signal-commits.json\n\n{\n echo \"## UK AI Governance Pre-compute\"\n echo \"- Repository: ${REPO}\"\n echo \"- Lookback days: ${DAYS}\"\n echo \"- Since: ${SINCE}\"\n echo \"- Commits in window: $(jq 'length' /tmp/gh-aw/agent/recent-commits.json)\"\n echo \"- Security-signal commits: $(jq 'length' /tmp/gh-aw/agent/security-signal-commits.json)\"\n echo \"- Open security issues: $(jq 'length' /tmp/gh-aw/agent/open-security-issues.json)\"\n echo \"- Open code scanning alerts: $(jq 'length' /tmp/gh-aw/agent/open-code-scanning-alerts.json)\"\n echo \"- Open secret scanning alerts: $(jq 'length' /tmp/gh-aw/agent/open-secret-scanning-alerts.json)\"\n} > /tmp/gh-aw/agent/uk-ai-governance-context.md"
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.24@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4
diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml
index de8aaf12c94..a533fc9776c 100644
--- a/.github/workflows/unbloat-docs.lock.yml
+++ b/.github/workflows/unbloat-docs.lock.yml
@@ -635,7 +635,7 @@ jobs:
GH_AW_SKILL_DIR: ".pi/skills"
run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh"
- name: Pre-flight checks
- run: "mkdir -p /tmp/gh-aw/agent\nmkdir -p /tmp/gh-aw/cache-memory\n\n# Write a heartbeat timestamp so the cache always has fresh content to save,\n# even on noop runs where the agent writes nothing to the cache directory.\necho \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" > /tmp/gh-aw/cache-memory/last-run.txt\n\n# Check 1: verify docs directory structure exists\nDIR_COUNT=$(find docs/src/content/docs -maxdepth 1 -type d 2>/dev/null | wc -l)\nif [ \"$DIR_COUNT\" -eq 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight failed: docs/src/content/docs directory not found ā documentation structure is missing or repository is not set up correctly.\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# Check 2: count editable markdown files\nTOTAL=$(find docs/src/content/docs -path '*/blog*' -prune \\\n -o -name '*.md' -type f ! -name 'frontmatter-full.md' -print \\\n | xargs grep -rL 'disable-agentic-editing: true' 2>/dev/null \\\n | wc -l)\nif [ \"$TOTAL\" -eq 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight failed: no editable markdown files found in docs/src/content/docs (all files may be protected or excluded).\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# Check 3: count uncleaned candidates (not cleaned in the past 7 days)\nRECENT_CUTOFF=$(date -d '7 days ago' '+%Y-%m-%d' 2>/dev/null \\\n || date -v-7d '+%Y-%m-%d' 2>/dev/null \\\n || echo \"0000-00-00\")\n\n# Expiration check: if the most recent cleanup entry is older than 14 days the\n# cache has gone cold (e.g. GitHub Actions evicted the 7-day cache entry).\n# Reset cleaned-files.txt so every file is eligible again and stale \"already\n# cleaned\" claims are not silently reused.\nCACHE_FILE=\"/tmp/gh-aw/cache-memory/cleaned-files.txt\"\nSTALE_CUTOFF=$(date -d '14 days ago' '+%Y-%m-%d' 2>/dev/null \\\n || date -v-14d '+%Y-%m-%d' 2>/dev/null \\\n || echo \"0000-00-00\")\nLATEST_ENTRY=$(awk 'NF>0{print $1}' \"$CACHE_FILE\" 2>/dev/null | sort | tail -1)\nif [ -n \"$LATEST_ENTRY\" ] && [ \"$LATEST_ENTRY\" \\< \"$STALE_CUTOFF\" ]; then\n echo \"Cache expiration: most recent entry $LATEST_ENTRY predates $STALE_CUTOFF ā resetting cleaned-files.txt\"\n > \"$CACHE_FILE\"\nfi\n\nCLEANED=$(awk -v cutoff=\"$RECENT_CUTOFF\" \\\n 'NF>0 && $1>=cutoff{count++} END{print count+0}' \\\n \"$CACHE_FILE\" 2>/dev/null || echo \"0\")\nUNCLEANED=$(( TOTAL - CLEANED ))\nif [ \"$UNCLEANED\" -le 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight check: all eligible documentation files were cleaned recently ā nothing to do this run.\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# All checks passed ā write candidate file list and preflight result\nfind docs/src/content/docs -path '*/blog*' -prune \\\n -o -name '*.md' -type f ! -name 'frontmatter-full.md' -print \\\n | xargs grep -rL 'disable-agentic-editing: true' 2>/dev/null \\\n > /tmp/gh-aw/agent/candidate-files.txt\nprintf '{\"pass\":true,\"reason\":\"All pre-flight checks passed. %d uncleaned candidates available.\",\"uncleaned\":%d,\"total\":%d}\\n' \\\n \"$UNCLEANED\" \"$UNCLEANED\" \"$TOTAL\" \\\n > /tmp/gh-aw/agent/preflight.json\n\necho \"Pre-flight passed: $UNCLEANED uncleaned candidates out of $TOTAL eligible files\"\necho \"Candidate files written to /tmp/gh-aw/agent/candidate-files.txt\"\n"
+ run: "mkdir -p /tmp/gh-aw/agent\nmkdir -p /tmp/gh-aw/cache-memory\n\n# Write a heartbeat timestamp so the cache always has fresh content to save,\n# even on noop runs where the agent writes nothing to the cache directory.\necho \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" > /tmp/gh-aw/cache-memory/last-run.txt\n\n# Check 1: verify docs directory structure exists\nDIR_COUNT=$(find docs/src/content/docs -maxdepth 1 -type d 2>/dev/null | wc -l)\nif [ \"$DIR_COUNT\" -eq 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight failed: docs/src/content/docs directory not found ā documentation structure is missing or repository is not set up correctly.\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# Check 2: count editable markdown files\nTOTAL=$(find docs/src/content/docs -path '*/blog*' -prune \\\n -o -name '*.md' -type f ! -name 'frontmatter-full.md' -print \\\n | xargs grep -rL 'disable-agentic-editing: true' 2>/dev/null \\\n | wc -l)\nif [ \"$TOTAL\" -eq 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight failed: no editable markdown files found in docs/src/content/docs (all files may be protected or excluded).\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# Check 3: count uncleaned candidates (not cleaned in the past 7 days)\nRECENT_CUTOFF=$(date -d '7 days ago' '+%Y-%m-%d' 2>/dev/null \\\n || date -v-7d '+%Y-%m-%d' 2>/dev/null \\\n || echo \"0000-00-00\")\n\n# Expiration check: if the most recent cleanup entry is older than 14 days the\n# cache has gone cold (e.g. GitHub Actions evicted the 7-day cache entry).\n# Reset cleaned-files.txt so every file is eligible again and stale \"already\n# cleaned\" claims are not silently reused.\nCACHE_FILE=\"/tmp/gh-aw/cache-memory/cleaned-files.txt\"\nSTALE_CUTOFF=$(date -d '14 days ago' '+%Y-%m-%d' 2>/dev/null \\\n || date -v-14d '+%Y-%m-%d' 2>/dev/null \\\n || echo \"0000-00-00\")\nLATEST_ENTRY=$(awk 'NF>0{print $1}' \"$CACHE_FILE\" 2>/dev/null | sort | tail -1)\nif [ -n \"$LATEST_ENTRY\" ] && [ \"$LATEST_ENTRY\" \\< \"$STALE_CUTOFF\" ]; then\n echo \"Cache expiration: most recent entry $LATEST_ENTRY predates $STALE_CUTOFF ā resetting cleaned-files.txt\"\n > \"$CACHE_FILE\"\nfi\n\nCLEANED=$(awk -v cutoff=\"$RECENT_CUTOFF\" \\\n 'NF>0 && $1>=cutoff{count++} END{print count+0}' \\\n \"$CACHE_FILE\" 2>/dev/null || echo \"0\")\nUNCLEANED=$(( TOTAL - CLEANED ))\nif [ \"$UNCLEANED\" -le 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight check: all eligible documentation files were cleaned recently ā nothing to do this run.\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# All checks passed ā write candidate file list and preflight result\nfind docs/src/content/docs -path '*/blog*' -prune \\\n -o -name '*.md' -type f ! -name 'frontmatter-full.md' -print \\\n | xargs grep -rL 'disable-agentic-editing: true' 2>/dev/null \\\n > /tmp/gh-aw/agent/candidate-files.txt\nprintf '{\"pass\":true,\"reason\":\"All pre-flight checks passed. %d uncleaned candidates available.\",\"uncleaned\":%d,\"total\":%d}\\n' \\\n \"$UNCLEANED\" \"$UNCLEANED\" \"$TOTAL\" \\\n > /tmp/gh-aw/agent/preflight.json\n\necho \"Pre-flight passed: $UNCLEANED uncleaned candidates out of $TOTAL eligible files\"\necho \"Candidate files written to /tmp/gh-aw/agent/candidate-files.txt\""
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.24@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4
diff --git a/.github/workflows/weekly-blog-post-writer.lock.yml b/.github/workflows/weekly-blog-post-writer.lock.yml
index 896aead4e69..d992b9bd3de 100644
--- a/.github/workflows/weekly-blog-post-writer.lock.yml
+++ b/.github/workflows/weekly-blog-post-writer.lock.yml
@@ -583,7 +583,7 @@ jobs:
bash "${RUNNER_TEMP}/gh-aw/actions/start_difc_proxy.sh"
- name: Pre-fetch release and merged PR data
if: ${{ needs.activation.outputs.prefetch_strategy == 'eager' }}
- run: |
+ run: |-
set -euo pipefail
mkdir -p /tmp/gh-aw/agent
diff --git a/.github/workflows/workflow-health-manager.lock.yml b/.github/workflows/workflow-health-manager.lock.yml
index 6c1a64a7b82..dfc54330fcf 100644
--- a/.github/workflows/workflow-health-manager.lock.yml
+++ b/.github/workflows/workflow-health-manager.lock.yml
@@ -490,7 +490,7 @@ jobs:
- env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Build Inventory
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n# Run compilation validation and capture output\ngh aw compile --validate > /tmp/gh-aw/agent/compile-validate.txt 2>&1 || true\n# List executable workflow files (exclude shared/ subdirectory)\nls .github/workflows/*.md 2>/dev/null > /tmp/gh-aw/agent/workflow-list.txt || true\necho \"Inventory complete: $(wc -l < /tmp/gh-aw/agent/workflow-list.txt | tr -d ' ') workflows found\"\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\n# Run compilation validation and capture output\ngh aw compile --validate > /tmp/gh-aw/agent/compile-validate.txt 2>&1 || true\n# List executable workflow files (exclude shared/ subdirectory)\nls .github/workflows/*.md 2>/dev/null > /tmp/gh-aw/agent/workflow-list.txt || true\necho \"Inventory complete: $(wc -l < /tmp/gh-aw/agent/workflow-list.txt | tr -d ' ') workflows found\""
# Repo memory git-based storage configuration from frontmatter processed below
- name: Clone repo-memory branch (default)
@@ -559,7 +559,7 @@ jobs:
GH_AW_SKILL_DIR: ".github/skills"
run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh"
- name: Load Metrics
- run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\nMETRICS_FILE=\"/tmp/gh-aw/repo-memory/default/metrics/latest.json\"\nif [ -f \"$METRICS_FILE\" ]; then\n jq '[.workflow_runs | to_entries[]\n | select(.value.success_rate < 0.8)]\n | sort_by(.value.success_rate) | .[0:20]' \\\n \"$METRICS_FILE\" > /tmp/gh-aw/agent/failing-workflows.json 2>/dev/null \\\n || echo '[]' > /tmp/gh-aw/agent/failing-workflows.json\nelse\n echo '[]' > /tmp/gh-aw/agent/failing-workflows.json\nfi\necho \"Metrics loaded: $(jq 'length' /tmp/gh-aw/agent/failing-workflows.json) failing workflows (<80% success)\"\n"
+ run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\nMETRICS_FILE=\"/tmp/gh-aw/repo-memory/default/metrics/latest.json\"\nif [ -f \"$METRICS_FILE\" ]; then\n jq '[.workflow_runs | to_entries[]\n | select(.value.success_rate < 0.8)]\n | sort_by(.value.success_rate) | .[0:20]' \\\n \"$METRICS_FILE\" > /tmp/gh-aw/agent/failing-workflows.json 2>/dev/null \\\n || echo '[]' > /tmp/gh-aw/agent/failing-workflows.json\nelse\n echo '[]' > /tmp/gh-aw/agent/failing-workflows.json\nfi\necho \"Metrics loaded: $(jq 'length' /tmp/gh-aw/agent/failing-workflows.json) failing workflows (<80% success)\""
- name: Download container images
run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.24@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c ghcr.io/github/gh-aw-firewall/api-proxy:0.27.24@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.24@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b ghcr.io/github/gh-aw-firewall/squid:0.27.24@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485 ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4
diff --git a/.github/workflows/workflow-skill-extractor.lock.yml b/.github/workflows/workflow-skill-extractor.lock.yml
index ec5683faf20..9a7bd276653 100644
--- a/.github/workflows/workflow-skill-extractor.lock.yml
+++ b/.github/workflows/workflow-skill-extractor.lock.yml
@@ -475,7 +475,7 @@ jobs:
- name: Build workflow index
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
- script: "const fs = require('fs');\nconst path = require('path');\n\nconst workflowDir = '.github/workflows';\nconst entries = fs.readdirSync(workflowDir, { withFileTypes: true });\nconst index = [];\n\nfor (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n if (!entry.isFile() || !entry.name.endsWith('.md') || entry.name.startsWith('.')) {\n continue;\n }\n\n const workflowPath = path.join(workflowDir, entry.name);\n const content = fs.readFileSync(workflowPath, 'utf8');\n const frontmatterMatch = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n const frontmatter = frontmatterMatch ? frontmatterMatch[1] : '';\n\n const imports = Array.from(frontmatter.matchAll(/^\\s*-\\s+(shared\\/\\S+)/gm), (m) => m[1]);\n let engine = null;\n const frontmatterLines = frontmatter.split('\\n');\n let inEngineBlock = false;\n\n for (const line of frontmatterLines) {\n if (!inEngineBlock) {\n if (/^engine:\\s*$/.test(line)) {\n inEngineBlock = true;\n }\n continue;\n }\n\n if (!/^[ \\t]/.test(line)) {\n break;\n }\n\n const engineIDMatch = line.match(/^\\s*id:\\s*(\\S+)/);\n if (engineIDMatch) {\n engine = engineIDMatch[1];\n break;\n }\n }\n\n index.push({\n file: entry.name,\n path: workflowPath,\n imports,\n engine,\n has_github_tools: frontmatter.includes('github:'),\n has_safe_outputs: frontmatter.includes('safe-outputs:'),\n frontmatter_preview: frontmatter.slice(0, 400)\n });\n}\n\nfs.mkdirSync('/tmp/gh-aw/agent', { recursive: true });\nfs.writeFileSync('/tmp/gh-aw/agent/workflow-index.json', JSON.stringify(index, null, 2) + '\\n', 'utf8');\ncore.info(`Indexed ${index.length} workflows`);\n"
+ script: "const fs = require('fs');\nconst path = require('path');\n\nconst workflowDir = '.github/workflows';\nconst entries = fs.readdirSync(workflowDir, { withFileTypes: true });\nconst index = [];\n\nfor (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n if (!entry.isFile() || !entry.name.endsWith('.md') || entry.name.startsWith('.')) {\n continue;\n }\n\n const workflowPath = path.join(workflowDir, entry.name);\n const content = fs.readFileSync(workflowPath, 'utf8');\n const frontmatterMatch = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n const frontmatter = frontmatterMatch ? frontmatterMatch[1] : '';\n\n const imports = Array.from(frontmatter.matchAll(/^\\s*-\\s+(shared\\/\\S+)/gm), (m) => m[1]);\n let engine = null;\n const frontmatterLines = frontmatter.split('\\n');\n let inEngineBlock = false;\n\n for (const line of frontmatterLines) {\n if (!inEngineBlock) {\n if (/^engine:\\s*$/.test(line)) {\n inEngineBlock = true;\n }\n continue;\n }\n\n if (!/^[ \\t]/.test(line)) {\n break;\n }\n\n const engineIDMatch = line.match(/^\\s*id:\\s*(\\S+)/);\n if (engineIDMatch) {\n engine = engineIDMatch[1];\n break;\n }\n }\n\n index.push({\n file: entry.name,\n path: workflowPath,\n imports,\n engine,\n has_github_tools: frontmatter.includes('github:'),\n has_safe_outputs: frontmatter.includes('safe-outputs:'),\n frontmatter_preview: frontmatter.slice(0, 400)\n });\n}\n\nfs.mkdirSync('/tmp/gh-aw/agent', { recursive: true });\nfs.writeFileSync('/tmp/gh-aw/agent/workflow-index.json', JSON.stringify(index, null, 2) + '\\n', 'utf8');\ncore.info(`Indexed ${index.length} workflows`);"
- name: Configure Git credentials
env:
diff --git a/actions/setup/js/log_parser_format.cjs b/actions/setup/js/log_parser_format.cjs
index c22c3a75819..99384688ced 100644
--- a/actions/setup/js/log_parser_format.cjs
+++ b/actions/setup/js/log_parser_format.cjs
@@ -1,5 +1,7 @@
// @ts-check
+const { buildStepSummaryDetailsSection } = require("./log_parser_step_summary_builder.cjs");
+
/**
* Minimal dependency contract injected from log_parser_shared.cjs.
* Keeping this explicit helps prevent silent drift between modules.
@@ -87,27 +89,6 @@ function createLogParserFormatters(deps) {
return logEntries;
}
- /**
- * Builds a step-summary section with a collapsible details body whose summary
- * acts as the section header.
- *
- * NOTE: This is a local copy of log_parser_shared.cjs:buildStepSummaryDetailsSection.
- * It cannot be imported from there because log_parser_shared.cjs requires
- * log_parser_format.cjs (circular dependency). Keep both copies in sync.
- *
- * @param {string} title
- * @param {string} body
- * @param {{open?: boolean, emptyBodyMessage?: string}} [options]
- * @returns {string}
- */
- function buildStepSummaryDetailsSection(title, body, options = {}) {
- const { open = false, emptyBodyMessage = "No details available." } = options;
- const openAttr = open ? " open" : "";
- const trimmedBody = typeof body === "string" ? body.trim() : "";
- const content = trimmedBody || emptyBodyMessage;
- return `\n${title}
\n\n${content}\n \n\n`;
- }
-
/**
* Generates markdown summary from conversation log entries
* This is the core shared logic between Claude and Copilot log parsers
diff --git a/actions/setup/js/log_parser_shared.cjs b/actions/setup/js/log_parser_shared.cjs
index 618c355ae32..e07d4cc6bc4 100644
--- a/actions/setup/js/log_parser_shared.cjs
+++ b/actions/setup/js/log_parser_shared.cjs
@@ -5,6 +5,7 @@ const { getErrorMessage } = require("./error_helpers.cjs");
const { unfenceMarkdown } = require("./markdown_unfencing.cjs");
const { ERR_PARSE } = require("./error_codes.cjs");
const createLogParserFormatters = require("./log_parser_format.cjs");
+const { buildStepSummaryDetailsSection } = require("./log_parser_step_summary_builder.cjs");
/**
* Shared utility functions for log parsers
@@ -270,22 +271,6 @@ function isLikelyCustomAgent(toolName) {
return true;
}
-/**
- * Builds a step-summary section with a collapsible details body whose summary
- * acts as the section header.
- * @param {string} title
- * @param {string} body
- * @param {{open?: boolean, emptyBodyMessage?: string}} [options]
- * @returns {string}
- */
-function buildStepSummaryDetailsSection(title, body, options = {}) {
- const { open = false, emptyBodyMessage = "No details available." } = options;
- const openAttr = open ? " open" : "";
- const trimmedBody = typeof body === "string" ? body.trim() : "";
- const content = trimmedBody || emptyBodyMessage;
- return `\n${title}
\n\n${content}\n \n\n`;
-}
-
/**
* Generates information section markdown from the last log entry
* @param {any} lastEntry - The last log entry with metadata (num_turns, duration_ms, etc.)
diff --git a/actions/setup/js/log_parser_step_summary_builder.cjs b/actions/setup/js/log_parser_step_summary_builder.cjs
new file mode 100644
index 00000000000..ef7ad333574
--- /dev/null
+++ b/actions/setup/js/log_parser_step_summary_builder.cjs
@@ -0,0 +1,19 @@
+// @ts-check
+
+/**
+ * Builds a step-summary section with a collapsible details body whose summary
+ * acts as the section header.
+ * @param {string} title
+ * @param {string} body
+ * @param {{open?: boolean, emptyBodyMessage?: string}} [options]
+ * @returns {string}
+ */
+function buildStepSummaryDetailsSection(title, body, options = {}) {
+ const { open = false, emptyBodyMessage = "No details available." } = options;
+ const openAttr = open ? " open" : "";
+ const trimmedBody = typeof body === "string" ? body.trim() : "";
+ const content = trimmedBody || emptyBodyMessage;
+ return `\n${title}
\n\n${content}\n \n\n`;
+}
+
+module.exports = { buildStepSummaryDetailsSection };