From b9799eef090f2592b3c973ff75c2a39c0de7f2e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:30:28 +0000 Subject: [PATCH 01/14] Initial plan From 43091955ed7395e154ff593aa665aa2f55d91469 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:47:50 +0000 Subject: [PATCH 02/14] fix: improve Claude empty-startup diagnostics and retry handling Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 45 ++++++++++++- actions/setup/js/claude_harness.test.cjs | 27 ++++++++ actions/setup/js/log_parser_bootstrap.cjs | 65 ++++++++++++++++++- .../setup/js/log_parser_bootstrap.test.cjs | 31 ++++++++- actions/setup/js/parse_claude_log.test.cjs | 4 +- 5 files changed, 166 insertions(+), 6 deletions(-) diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index 142eaaa3246..1b38d0dce4a 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -99,6 +99,35 @@ function resolveRetryConfig(env = process.env, logger = log) { return resolveSharedRetryConfig(env, logger); } +/** + * Parse bounded startup retries for zero-output startup failures. + * Retries are always fresh runs (never --continue). + * + * @param {NodeJS.ProcessEnv} [env] + * @param {(message: string) => void} [logger] + * @returns {number} + */ +function resolveStartupRetryLimit(env = process.env, logger = log) { + const raw = env.GH_AW_CLAUDE_STARTUP_RETRIES; + if (raw == null || raw === "") { + return 1; + } + if (!/^[+-]?\d+$/.test(raw)) { + logger(`invalid GH_AW_CLAUDE_STARTUP_RETRIES='${raw}' (expected integer); using default startup retries=1`); + return 1; + } + const parsed = Number.parseInt(raw, 10); + if (!Number.isFinite(parsed)) { + logger(`invalid GH_AW_CLAUDE_STARTUP_RETRIES='${raw}' (not finite); using default startup retries=1`); + return 1; + } + const clamped = Math.min(Math.max(parsed, 0), 2); + if (clamped !== parsed) { + logger(`GH_AW_CLAUDE_STARTUP_RETRIES=${parsed} out of range; clamped to ${clamped}`); + } + return clamped; +} + /** * Determines if the collected output contains an Anthropic overload error. * @param {string} output - Collected stdout+stderr from the process @@ -297,6 +326,7 @@ async function main() { const [, , command, ...args] = process.argv; const retryConfig = resolveRetryConfig(process.env, log); const { maxRetries, initialDelayMs, backoffMultiplier, maxDelayMs } = retryConfig; + const startupRetryLimit = resolveStartupRetryLimit(process.env, log); if (!command) { process.stderr.write("claude-harness: Usage: node claude_harness.cjs [args...]\n"); @@ -347,6 +377,7 @@ async function main() { let lastExitCode = 1; let useContinueOnRetry = false; let continueDisabledPermanently = false; + let startupRetriesUsed = 0; const driverStartTime = Date.now(); // Soft-timeout guard: polled at the top of the retry loop and after each backoff sleep. // It does not preempt a running attempt — if a single invocation runs past the soft @@ -519,7 +550,18 @@ async function main() { if (attempt >= maxRetries) { log(`all ${maxRetries} retries exhausted — giving up (exitCode=${lastExitCode})`); } else { - log(`attempt ${attempt + 1}: no output produced — not retrying` + ` (possible causes: binary not found, permission denied, auth failure, or silent startup crash)`); + if (startupRetriesUsed < startupRetryLimit) { + startupRetriesUsed++; + useContinueOnRetry = false; + continueDisabledPermanently = true; + log(`attempt ${attempt + 1}: no output produced — retrying startup as fresh run ` + `(startup retry ${startupRetriesUsed}/${startupRetryLimit}, attempt ${attempt + 2}/${maxRetries + 1})`); + continue; + } + log( + `attempt ${attempt + 1}: no output produced — not retrying` + + ` (startup retry budget exhausted: ${startupRetriesUsed}/${startupRetryLimit};` + + ` possible causes: binary not found, permission denied, auth failure, or silent startup crash)` + ); } break; @@ -551,6 +593,7 @@ if (typeof module !== "undefined" && module.exports) { hasNoopInSafeOutputs, hasExpectedSafeOutputs, resolveRetryConfig, + resolveStartupRetryLimit, }; } diff --git a/actions/setup/js/claude_harness.test.cjs b/actions/setup/js/claude_harness.test.cjs index 4d888d13808..c9d09dbf35d 100644 --- a/actions/setup/js/claude_harness.test.cjs +++ b/actions/setup/js/claude_harness.test.cjs @@ -21,6 +21,7 @@ const { extractDeniedCommands, buildMissingToolPermissionIssuePayload, resolveRetryConfig, + resolveStartupRetryLimit, } = require("./claude_harness.cjs"); const agentTempDir = "/tmp/gh-aw/agent"; @@ -448,6 +449,24 @@ process.exit(0); expect(result.stderr).toContain("failure_reason=cancelled_or_timed_out"); }, 30000); + it("retries one no-output startup failure as a fresh run by default", () => { + const stubScript = ` +const fs = require("fs"); +const callsPath = process.env.CLAUDE_HARNESS_STUB_CALLS; +const args = process.argv.slice(2); +const priorCalls = fs.existsSync(callsPath) ? fs.readFileSync(callsPath, "utf8").trim().split("\\n").filter(Boolean).length : 0; +fs.appendFileSync(callsPath, JSON.stringify({ args }) + "\\n", "utf8"); +if (priorCalls === 0) process.exit(1); +process.stdout.write("startup retry succeeded\\n"); +process.exit(0); +`; + const { result, calls } = runHarnessWithStub({ stubScript }); + expect(result.status, result.stderr).toBe(0); + expect(calls.length).toBe(2); + expect(calls[1].args.includes("--continue")).toBe(false); + expect(result.stderr).toContain("no output produced — retrying startup as fresh run"); + }); + it("returns true for normal partial-execution retry", () => { const result = shouldRetryWithContinue({ attempt: 0, @@ -559,6 +578,14 @@ process.exit(0); const config2 = resolveRetryConfig({ GH_AW_HARNESS_INITIAL_DELAY_MS: "0x10" }); expect(config2.initialDelayMs).toBe(5000); }); + + it("uses startup retry default and clamps overrides to [0..2]", () => { + expect(resolveStartupRetryLimit({})).toBe(1); + expect(resolveStartupRetryLimit({ GH_AW_CLAUDE_STARTUP_RETRIES: "2" })).toBe(2); + expect(resolveStartupRetryLimit({ GH_AW_CLAUDE_STARTUP_RETRIES: "-5" })).toBe(0); + expect(resolveStartupRetryLimit({ GH_AW_CLAUDE_STARTUP_RETRIES: "9" })).toBe(2); + expect(resolveStartupRetryLimit({ GH_AW_CLAUDE_STARTUP_RETRIES: "nope" })).toBe(1); + }); }); describe("noop pre-flight and retry guard", () => { diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index 26d420596d0..c1032ef3970 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -4,6 +4,54 @@ const { generatePlainTextSummary, generateCopilotCliStyleSummary, wrapAgentLogInSection, formatSafeOutputsPreview } = require("./log_parser_shared.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); +const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/i; +const CLAUDE_RATE_LIMIT_OR_OVERLOAD_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit|overloaded_error|"overloaded"|5\d\d/i; + +/** + * Build startup diagnostics for Claude failures with no structured entries. + * @param {string} rawContent + * @returns {{ + * exitCode: string, + * inferenceAccessError: boolean, + * aiCreditsRateLimitError: boolean, + * summaryLine: string, + * summaryMarkdown: string + * }} + */ +function buildClaudeStartupDiagnostics(rawContent) { + const content = typeof rawContent === "string" ? rawContent : ""; + const lines = content + .split(/\r?\n/) + .map(line => line.trim()) + .filter(Boolean); + const startupLines = lines.filter(line => line.includes("[claude-harness]") || /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|5\d\d|overloaded|inference)/i.test(line)); + const tailLines = (startupLines.length > 0 ? startupLines : lines).slice(-8); + const tailText = tailLines.join("\n"); + + let exitCode = "unknown"; + const doneMatches = [...content.matchAll(/done:\s*exitCode=(\d+)/g)]; + if (doneMatches.length > 0) { + exitCode = doneMatches[doneMatches.length - 1][1]; + } else { + const attemptMatches = [...content.matchAll(/attempt\s+\d+\s+failed:\s+exitCode=(\d+)/g)]; + if (attemptMatches.length > 0) { + exitCode = attemptMatches[attemptMatches.length - 1][1]; + } + } + + const inferenceAccessError = INFERENCE_ACCESS_ERROR_PATTERN.test(content); + const aiCreditsRateLimitError = CLAUDE_RATE_LIMIT_OR_OVERLOAD_PATTERN.test(content); + const summaryLine = `Claude startup failed before structured logging (exitCode=${exitCode}).`; + const summaryMarkdown = tailText ? `
Claude startup diagnostics\n\n\`\`\`text\n${tailText}\n\`\`\`\n
` : ""; + + return { + exitCode, + inferenceAccessError, + aiCreditsRateLimitError, + summaryLine, + summaryMarkdown, + }; +} /** * Bootstrap helper for log parser entry points. @@ -344,7 +392,22 @@ async function runLogParser(options) { `Claude produced no structured log entries, but agent completed with ${safeOutputEntriesCount} safe output ${safeOutputEntriesCount === 1 ? "entry" : "entries"} — treating as non-fatal post-completion infrastructure failure` ); } else { - core.setFailed(`${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution.`); + const diagnostics = buildClaudeStartupDiagnostics(content); + if (diagnostics.summaryMarkdown) { + await core.summary.addRaw(diagnostics.summaryMarkdown).write(); + } + + if (diagnostics.inferenceAccessError) { + core.setOutput("inference_access_error", "true"); + } + if (diagnostics.aiCreditsRateLimitError) { + core.setOutput("ai_credits_rate_limit_error", "true"); + } + + const errorCode = diagnostics.inferenceAccessError || diagnostics.aiCreditsRateLimitError ? ERR_API : ERR_CONFIG; + const failureKind = diagnostics.inferenceAccessError || diagnostics.aiCreditsRateLimitError ? "transient inference availability signal detected" : "startup/configuration failure detected"; + + core.setFailed(`${errorCode}: Claude execution failed: no structured log entries were produced. ${diagnostics.summaryLine} ${failureKind}.`); } } diff --git a/actions/setup/js/log_parser_bootstrap.test.cjs b/actions/setup/js/log_parser_bootstrap.test.cjs index 19f9685a9f8..8eff4688e3c 100644 --- a/actions/setup/js/log_parser_bootstrap.test.cjs +++ b/actions/setup/js/log_parser_bootstrap.test.cjs @@ -71,7 +71,9 @@ describe("log_parser_bootstrap.cjs", () => { process.env.GH_AW_AGENT_OUTPUT = logFile; const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] }); await runLogParser({ parseLog: mockParseLog, parserName: "Claude" }); - expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution.`); + expect(mockCore.setFailed).toHaveBeenCalledWith( + `${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. Claude startup failed before structured logging (exitCode=unknown). startup/configuration failure detected.` + ); } finally { fs.unlinkSync(logFile); fs.rmdirSync(tmpDir); @@ -237,7 +239,9 @@ describe("log_parser_bootstrap.cjs", () => { delete process.env.GH_AW_SAFE_OUTPUTS; const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] }); await runLogParser({ parseLog: mockParseLog, parserName: "Claude" }); - expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution.`); + expect(mockCore.setFailed).toHaveBeenCalledWith( + `${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. Claude startup failed before structured logging (exitCode=unknown). startup/configuration failure detected.` + ); } finally { fs.unlinkSync(logFile); fs.rmdirSync(tmpDir); @@ -274,7 +278,28 @@ describe("log_parser_bootstrap.cjs", () => { delete process.env.GH_AW_SAFE_OUTPUTS; const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: null }); await runLogParser({ parseLog: mockParseLog, parserName: "Claude" }); - expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution.`); + expect(mockCore.setFailed).toHaveBeenCalledWith( + `${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. Claude startup failed before structured logging (exitCode=unknown). startup/configuration failure detected.` + ); + } finally { + fs.unlinkSync(logFile); + delete process.env.GH_AW_AGENT_OUTPUT; + fs.rmdirSync(tmpDir); + } + }), + it("should classify Claude empty-log startup rate-limit signatures as transient inference availability", async () => { + const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-")); + const logFile = path.join(tmpDir, "test.log"); + try { + fs.writeFileSync(logFile, `[claude-harness] attempt 1 failed: exitCode=1 isRateLimitError=true hasOutput=false\n[claude-harness] done: exitCode=1\nAPI Error: Request rejected (429)`); + process.env.GH_AW_AGENT_OUTPUT = logFile; + delete process.env.GH_AW_SAFE_OUTPUTS; + const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] }); + await runLogParser({ parseLog: mockParseLog, parserName: "Claude" }); + expect(mockCore.setFailed).toHaveBeenCalledWith( + `${ERR_API}: Claude execution failed: no structured log entries were produced. Claude startup failed before structured logging (exitCode=1). transient inference availability signal detected.` + ); + expect(mockCore.setOutput).toHaveBeenCalledWith("ai_credits_rate_limit_error", "true"); } finally { fs.unlinkSync(logFile); delete process.env.GH_AW_AGENT_OUTPUT; diff --git a/actions/setup/js/parse_claude_log.test.cjs b/actions/setup/js/parse_claude_log.test.cjs index 09baa6d21bb..bd724a276c9 100644 --- a/actions/setup/js/parse_claude_log.test.cjs +++ b/actions/setup/js/parse_claude_log.test.cjs @@ -441,7 +441,9 @@ describe("parse_claude_log.cjs", () => { it("should fail when Claude log has no structured entries", async () => { await runScript("this is not structured Claude JSON output"); - expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution.`); + expect(mockCore.setFailed).toHaveBeenCalledWith( + `${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. Claude startup failed before structured logging (exitCode=unknown). startup/configuration failure detected.` + ); }); }); From 0ccd4ab36f801ad240bb26657ca980a9778d597f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:51:49 +0000 Subject: [PATCH 03/14] refine: tighten startup signal matching and retry behavior Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 1 - actions/setup/js/log_parser_bootstrap.cjs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index 1b38d0dce4a..f93648b9237 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -553,7 +553,6 @@ async function main() { if (startupRetriesUsed < startupRetryLimit) { startupRetriesUsed++; useContinueOnRetry = false; - continueDisabledPermanently = true; log(`attempt ${attempt + 1}: no output produced — retrying startup as fresh run ` + `(startup retry ${startupRetriesUsed}/${startupRetryLimit}, attempt ${attempt + 2}/${maxRetries + 1})`); continue; } diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index c1032ef3970..497ec39288a 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -5,7 +5,7 @@ const { generatePlainTextSummary, generateCopilotCliStyleSummary, wrapAgentLogIn const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/i; -const CLAUDE_RATE_LIMIT_OR_OVERLOAD_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit|overloaded_error|"overloaded"|5\d\d/i; +const CLAUDE_RATE_LIMIT_OR_OVERLOAD_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit|overloaded_error|"overloaded"|(?:HTTP|status|error)[^\n]{0,60}\b5\d\d\b/i; /** * Build startup diagnostics for Claude failures with no structured entries. From c87d2807b89ed0f0b0ca32b27fb0e9b1a64c432a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:55:46 +0000 Subject: [PATCH 04/14] chore: address review feedback on startup diagnostics Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 2 +- actions/setup/js/log_parser_bootstrap.cjs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index f93648b9237..e9b630b278d 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -553,7 +553,7 @@ async function main() { if (startupRetriesUsed < startupRetryLimit) { startupRetriesUsed++; useContinueOnRetry = false; - log(`attempt ${attempt + 1}: no output produced — retrying startup as fresh run ` + `(startup retry ${startupRetriesUsed}/${startupRetryLimit}, attempt ${attempt + 2}/${maxRetries + 1})`); + log(`attempt ${attempt + 1}: no output produced — retrying startup as fresh run ` + `(startup retry ${startupRetriesUsed}/${startupRetryLimit}, next attempt ${attempt + 2} of ${maxRetries + 1} total attempts)`); continue; } log( diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index 497ec39288a..b9270f746c7 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -5,7 +5,10 @@ const { generatePlainTextSummary, generateCopilotCliStyleSummary, wrapAgentLogIn const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/i; -const CLAUDE_RATE_LIMIT_OR_OVERLOAD_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit|overloaded_error|"overloaded"|(?:HTTP|status|error)[^\n]{0,60}\b5\d\d\b/i; +const CLAUDE_RATE_LIMIT_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit/i; +const CLAUDE_OVERLOAD_PATTERN = /overloaded_error|"overloaded"/i; +const CLAUDE_HTTP_5XX_PATTERN = /(?:HTTP|status|error)[^\n]{0,60}\b5\d\d\b/i; +const STARTUP_DIAGNOSTIC_LINE_PATTERN = /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|5\d\d|overloaded|inference)/i; /** * Build startup diagnostics for Claude failures with no structured entries. @@ -24,7 +27,7 @@ function buildClaudeStartupDiagnostics(rawContent) { .split(/\r?\n/) .map(line => line.trim()) .filter(Boolean); - const startupLines = lines.filter(line => line.includes("[claude-harness]") || /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|5\d\d|overloaded|inference)/i.test(line)); + const startupLines = lines.filter(line => line.includes("[claude-harness]") || STARTUP_DIAGNOSTIC_LINE_PATTERN.test(line)); const tailLines = (startupLines.length > 0 ? startupLines : lines).slice(-8); const tailText = tailLines.join("\n"); @@ -40,7 +43,7 @@ function buildClaudeStartupDiagnostics(rawContent) { } const inferenceAccessError = INFERENCE_ACCESS_ERROR_PATTERN.test(content); - const aiCreditsRateLimitError = CLAUDE_RATE_LIMIT_OR_OVERLOAD_PATTERN.test(content); + const aiCreditsRateLimitError = CLAUDE_RATE_LIMIT_PATTERN.test(content) || CLAUDE_OVERLOAD_PATTERN.test(content) || CLAUDE_HTTP_5XX_PATTERN.test(content); const summaryLine = `Claude startup failed before structured logging (exitCode=${exitCode}).`; const summaryMarkdown = tailText ? `
Claude startup diagnostics\n\n\`\`\`text\n${tailText}\n\`\`\`\n
` : ""; From 2da23d0c78b57bd7bb53028d3f2c010b1cc1c761 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:59:26 +0000 Subject: [PATCH 05/14] chore: polish diagnostics constants and readability Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 3 ++- actions/setup/js/log_parser_bootstrap.cjs | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index e9b630b278d..8c3fcc2eeaf 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -78,6 +78,7 @@ const MAX_TURNS_EXIT_PATTERN = /"subtype"\s*:\s*"error_max_turns"/; // this path must not be retried via --continue (fall back to a fresh run if budget remains). const NO_DEFERRED_MARKER_PATTERN = /No deferred tool marker found/i; const SIGNAL_TERMINATION_EXIT_CODES = new Set([137, 143]); +const MAX_STARTUP_RETRIES = 2; /** * Emit a timestamped diagnostic log line to stderr. @@ -121,7 +122,7 @@ function resolveStartupRetryLimit(env = process.env, logger = log) { logger(`invalid GH_AW_CLAUDE_STARTUP_RETRIES='${raw}' (not finite); using default startup retries=1`); return 1; } - const clamped = Math.min(Math.max(parsed, 0), 2); + const clamped = Math.min(Math.max(parsed, 0), MAX_STARTUP_RETRIES); if (clamped !== parsed) { logger(`GH_AW_CLAUDE_STARTUP_RETRIES=${parsed} out of range; clamped to ${clamped}`); } diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index b9270f746c7..874bccee69b 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -9,6 +9,7 @@ const CLAUDE_RATE_LIMIT_PATTERN = /rate_limit_error|429 Too Many Requests|"api_e const CLAUDE_OVERLOAD_PATTERN = /overloaded_error|"overloaded"/i; const CLAUDE_HTTP_5XX_PATTERN = /(?:HTTP|status|error)[^\n]{0,60}\b5\d\d\b/i; const STARTUP_DIAGNOSTIC_LINE_PATTERN = /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|5\d\d|overloaded|inference)/i; +const MAX_DIAGNOSTIC_TAIL_LINES = 8; /** * Build startup diagnostics for Claude failures with no structured entries. @@ -28,7 +29,7 @@ function buildClaudeStartupDiagnostics(rawContent) { .map(line => line.trim()) .filter(Boolean); const startupLines = lines.filter(line => line.includes("[claude-harness]") || STARTUP_DIAGNOSTIC_LINE_PATTERN.test(line)); - const tailLines = (startupLines.length > 0 ? startupLines : lines).slice(-8); + const tailLines = (startupLines.length > 0 ? startupLines : lines).slice(-MAX_DIAGNOSTIC_TAIL_LINES); const tailText = tailLines.join("\n"); let exitCode = "unknown"; @@ -43,7 +44,10 @@ function buildClaudeStartupDiagnostics(rawContent) { } const inferenceAccessError = INFERENCE_ACCESS_ERROR_PATTERN.test(content); - const aiCreditsRateLimitError = CLAUDE_RATE_LIMIT_PATTERN.test(content) || CLAUDE_OVERLOAD_PATTERN.test(content) || CLAUDE_HTTP_5XX_PATTERN.test(content); + const matchedRateLimit = CLAUDE_RATE_LIMIT_PATTERN.test(content); + const matchedOverload = CLAUDE_OVERLOAD_PATTERN.test(content); + const matchedHTTP5xx = CLAUDE_HTTP_5XX_PATTERN.test(content); + const aiCreditsRateLimitError = matchedRateLimit || matchedOverload || matchedHTTP5xx; const summaryLine = `Claude startup failed before structured logging (exitCode=${exitCode}).`; const summaryMarkdown = tailText ? `
Claude startup diagnostics\n\n\`\`\`text\n${tailText}\n\`\`\`\n
` : ""; From 237a3941af21073af6984e209f1c0ade81cff5e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:03:22 +0000 Subject: [PATCH 06/14] chore: tighten startup parsing and logging efficiency Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 4 +++- actions/setup/js/log_parser_bootstrap.cjs | 27 ++++++++++++++--------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index 8c3fcc2eeaf..17dd9a36640 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -554,7 +554,9 @@ async function main() { if (startupRetriesUsed < startupRetryLimit) { startupRetriesUsed++; useContinueOnRetry = false; - log(`attempt ${attempt + 1}: no output produced — retrying startup as fresh run ` + `(startup retry ${startupRetriesUsed}/${startupRetryLimit}, next attempt ${attempt + 2} of ${maxRetries + 1} total attempts)`); + const nextAttempt = attempt + 2; + const totalAttempts = maxRetries + 1; + log(`attempt ${attempt + 1}: no output produced — retrying startup as fresh run ` + `(startup retry ${startupRetriesUsed}/${startupRetryLimit}, next attempt ${nextAttempt} of ${totalAttempts} total attempts)`); continue; } log( diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index 874bccee69b..3d6bfa7d2c9 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -33,21 +33,28 @@ function buildClaudeStartupDiagnostics(rawContent) { const tailText = tailLines.join("\n"); let exitCode = "unknown"; - const doneMatches = [...content.matchAll(/done:\s*exitCode=(\d+)/g)]; - if (doneMatches.length > 0) { - exitCode = doneMatches[doneMatches.length - 1][1]; + const donePrefix = "done: exitCode="; + const doneIdx = content.lastIndexOf(donePrefix); + if (doneIdx !== -1) { + const doneTail = content.slice(doneIdx + donePrefix.length); + const doneCode = doneTail.match(/^(\d+)/); + if (doneCode) { + exitCode = doneCode[1]; + } } else { - const attemptMatches = [...content.matchAll(/attempt\s+\d+\s+failed:\s+exitCode=(\d+)/g)]; - if (attemptMatches.length > 0) { - exitCode = attemptMatches[attemptMatches.length - 1][1]; + const failedPrefix = "failed: exitCode="; + const failedIdx = content.lastIndexOf(failedPrefix); + if (failedIdx !== -1) { + const failedTail = content.slice(failedIdx + failedPrefix.length); + const failedCode = failedTail.match(/^(\d+)/); + if (failedCode) { + exitCode = failedCode[1]; + } } } const inferenceAccessError = INFERENCE_ACCESS_ERROR_PATTERN.test(content); - const matchedRateLimit = CLAUDE_RATE_LIMIT_PATTERN.test(content); - const matchedOverload = CLAUDE_OVERLOAD_PATTERN.test(content); - const matchedHTTP5xx = CLAUDE_HTTP_5XX_PATTERN.test(content); - const aiCreditsRateLimitError = matchedRateLimit || matchedOverload || matchedHTTP5xx; + const aiCreditsRateLimitError = CLAUDE_RATE_LIMIT_PATTERN.test(content) || CLAUDE_OVERLOAD_PATTERN.test(content) || CLAUDE_HTTP_5XX_PATTERN.test(content); const summaryLine = `Claude startup failed before structured logging (exitCode=${exitCode}).`; const summaryMarkdown = tailText ? `
Claude startup diagnostics\n\n\`\`\`text\n${tailText}\n\`\`\`\n
` : ""; From d8e40a894dbb15781558604ebb3b20348e508a1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:07:16 +0000 Subject: [PATCH 07/14] chore: apply review-driven readability refinements Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 6 ++---- actions/setup/js/log_parser_bootstrap.cjs | 7 ++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index 17dd9a36640..9f14baf641e 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -556,13 +556,11 @@ async function main() { useContinueOnRetry = false; const nextAttempt = attempt + 2; const totalAttempts = maxRetries + 1; - log(`attempt ${attempt + 1}: no output produced — retrying startup as fresh run ` + `(startup retry ${startupRetriesUsed}/${startupRetryLimit}, next attempt ${nextAttempt} of ${totalAttempts} total attempts)`); + log(`attempt ${attempt + 1}: no output produced — retrying startup as fresh run (startup retry ${startupRetriesUsed}/${startupRetryLimit}, next attempt ${nextAttempt} of ${totalAttempts} total attempts)`); continue; } log( - `attempt ${attempt + 1}: no output produced — not retrying` + - ` (startup retry budget exhausted: ${startupRetriesUsed}/${startupRetryLimit};` + - ` possible causes: binary not found, permission denied, auth failure, or silent startup crash)` + `attempt ${attempt + 1}: no output produced — not retrying (startup retry budget exhausted: ${startupRetriesUsed}/${startupRetryLimit}; possible causes: binary not found, permission denied, auth failure, or silent startup crash)` ); } diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index 3d6bfa7d2c9..ad542020b54 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -7,7 +7,7 @@ const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/i; const CLAUDE_RATE_LIMIT_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit/i; const CLAUDE_OVERLOAD_PATTERN = /overloaded_error|"overloaded"/i; -const CLAUDE_HTTP_5XX_PATTERN = /(?:HTTP|status|error)[^\n]{0,60}\b5\d\d\b/i; +const CLAUDE_HTTP_5XX_STATUS_PATTERN = /(?:HTTP|status|error)[^\n]{0,60}\b5\d\d\b/i; const STARTUP_DIAGNOSTIC_LINE_PATTERN = /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|5\d\d|overloaded|inference)/i; const MAX_DIAGNOSTIC_TAIL_LINES = 8; @@ -29,7 +29,8 @@ function buildClaudeStartupDiagnostics(rawContent) { .map(line => line.trim()) .filter(Boolean); const startupLines = lines.filter(line => line.includes("[claude-harness]") || STARTUP_DIAGNOSTIC_LINE_PATTERN.test(line)); - const tailLines = (startupLines.length > 0 ? startupLines : lines).slice(-MAX_DIAGNOSTIC_TAIL_LINES); + const diagnosticLines = startupLines.length > 0 ? startupLines : lines; + const tailLines = diagnosticLines.slice(-MAX_DIAGNOSTIC_TAIL_LINES); const tailText = tailLines.join("\n"); let exitCode = "unknown"; @@ -54,7 +55,7 @@ function buildClaudeStartupDiagnostics(rawContent) { } const inferenceAccessError = INFERENCE_ACCESS_ERROR_PATTERN.test(content); - const aiCreditsRateLimitError = CLAUDE_RATE_LIMIT_PATTERN.test(content) || CLAUDE_OVERLOAD_PATTERN.test(content) || CLAUDE_HTTP_5XX_PATTERN.test(content); + const aiCreditsRateLimitError = CLAUDE_RATE_LIMIT_PATTERN.test(content) || CLAUDE_OVERLOAD_PATTERN.test(content) || CLAUDE_HTTP_5XX_STATUS_PATTERN.test(content); const summaryLine = `Claude startup failed before structured logging (exitCode=${exitCode}).`; const summaryMarkdown = tailText ? `
Claude startup diagnostics\n\n\`\`\`text\n${tailText}\n\`\`\`\n
` : ""; From 9cd8ddd03a7463f0abea1c8d2e09cd5467008678 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:11:19 +0000 Subject: [PATCH 08/14] chore: clarify transient classification semantics Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 1 + actions/setup/js/log_parser_bootstrap.cjs | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index 9f14baf641e..f79c2201121 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -554,6 +554,7 @@ async function main() { if (startupRetriesUsed < startupRetryLimit) { startupRetriesUsed++; useContinueOnRetry = false; + // attempt is 0-based; logs are 1-based, so "next attempt" is +2. const nextAttempt = attempt + 2; const totalAttempts = maxRetries + 1; log(`attempt ${attempt + 1}: no output produced — retrying startup as fresh run (startup retry ${startupRetriesUsed}/${startupRetryLimit}, next attempt ${nextAttempt} of ${totalAttempts} total attempts)`); diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index ad542020b54..a005ab56dee 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -7,7 +7,7 @@ const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/i; const CLAUDE_RATE_LIMIT_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit/i; const CLAUDE_OVERLOAD_PATTERN = /overloaded_error|"overloaded"/i; -const CLAUDE_HTTP_5XX_STATUS_PATTERN = /(?:HTTP|status|error)[^\n]{0,60}\b5\d\d\b/i; +const CLAUDE_HTTP_5XX_STATUS_PATTERN = /(?:HTTP|status|error)[^\n]{0,60}\b5\d{2}\b/i; const STARTUP_DIAGNOSTIC_LINE_PATTERN = /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|5\d\d|overloaded|inference)/i; const MAX_DIAGNOSTIC_TAIL_LINES = 8; @@ -18,6 +18,7 @@ const MAX_DIAGNOSTIC_TAIL_LINES = 8; * exitCode: string, * inferenceAccessError: boolean, * aiCreditsRateLimitError: boolean, + * transientInferenceAvailabilityError: boolean, * summaryLine: string, * summaryMarkdown: string * }} @@ -55,7 +56,8 @@ function buildClaudeStartupDiagnostics(rawContent) { } const inferenceAccessError = INFERENCE_ACCESS_ERROR_PATTERN.test(content); - const aiCreditsRateLimitError = CLAUDE_RATE_LIMIT_PATTERN.test(content) || CLAUDE_OVERLOAD_PATTERN.test(content) || CLAUDE_HTTP_5XX_STATUS_PATTERN.test(content); + const aiCreditsRateLimitError = CLAUDE_RATE_LIMIT_PATTERN.test(content) || CLAUDE_OVERLOAD_PATTERN.test(content); + const transientInferenceAvailabilityError = aiCreditsRateLimitError || CLAUDE_HTTP_5XX_STATUS_PATTERN.test(content); const summaryLine = `Claude startup failed before structured logging (exitCode=${exitCode}).`; const summaryMarkdown = tailText ? `
Claude startup diagnostics\n\n\`\`\`text\n${tailText}\n\`\`\`\n
` : ""; @@ -63,6 +65,7 @@ function buildClaudeStartupDiagnostics(rawContent) { exitCode, inferenceAccessError, aiCreditsRateLimitError, + transientInferenceAvailabilityError, summaryLine, summaryMarkdown, }; @@ -419,8 +422,8 @@ async function runLogParser(options) { core.setOutput("ai_credits_rate_limit_error", "true"); } - const errorCode = diagnostics.inferenceAccessError || diagnostics.aiCreditsRateLimitError ? ERR_API : ERR_CONFIG; - const failureKind = diagnostics.inferenceAccessError || diagnostics.aiCreditsRateLimitError ? "transient inference availability signal detected" : "startup/configuration failure detected"; + const errorCode = diagnostics.inferenceAccessError || diagnostics.transientInferenceAvailabilityError ? ERR_API : ERR_CONFIG; + const failureKind = diagnostics.inferenceAccessError || diagnostics.transientInferenceAvailabilityError ? "transient inference availability signal detected" : "startup/configuration failure detected"; core.setFailed(`${errorCode}: Claude execution failed: no structured log entries were produced. ${diagnostics.summaryLine} ${failureKind}.`); } From ee6d3be92010abc44c2140f67e54e08d1ddfccee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:15:21 +0000 Subject: [PATCH 09/14] chore: refine regex precision and naming clarity Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 4 ++-- actions/setup/js/log_parser_bootstrap.cjs | 4 ++-- actions/setup/js/log_parser_bootstrap.test.cjs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index f79c2201121..12841af0af5 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -555,9 +555,9 @@ async function main() { startupRetriesUsed++; useContinueOnRetry = false; // attempt is 0-based; logs are 1-based, so "next attempt" is +2. - const nextAttempt = attempt + 2; + const nextAttemptNumber = attempt + 2; const totalAttempts = maxRetries + 1; - log(`attempt ${attempt + 1}: no output produced — retrying startup as fresh run (startup retry ${startupRetriesUsed}/${startupRetryLimit}, next attempt ${nextAttempt} of ${totalAttempts} total attempts)`); + log(`attempt ${attempt + 1}: no output produced — retrying startup as fresh run ` + `(startup retry ${startupRetriesUsed}/${startupRetryLimit}, next attempt ${nextAttemptNumber} of ${totalAttempts} total attempts)`); continue; } log( diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index a005ab56dee..f9e3998612a 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -7,8 +7,8 @@ const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/i; const CLAUDE_RATE_LIMIT_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit/i; const CLAUDE_OVERLOAD_PATTERN = /overloaded_error|"overloaded"/i; -const CLAUDE_HTTP_5XX_STATUS_PATTERN = /(?:HTTP|status|error)[^\n]{0,60}\b5\d{2}\b/i; -const STARTUP_DIAGNOSTIC_LINE_PATTERN = /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|5\d\d|overloaded|inference)/i; +const CLAUDE_HTTP_5XX_STATUS_PATTERN = /(?:HTTP(?:\/\d\.\d)?\s+5\d{2}\b|status(?:\s+code)?\s*[:=]\s*5\d{2}\b|error(?:\s+code)?\s*[:=]?\s*5\d{2}\b)/i; +const STARTUP_DIAGNOSTIC_LINE_PATTERN = /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|\b5\d{2}\b|overloaded|inference)/i; const MAX_DIAGNOSTIC_TAIL_LINES = 8; /** diff --git a/actions/setup/js/log_parser_bootstrap.test.cjs b/actions/setup/js/log_parser_bootstrap.test.cjs index 8eff4688e3c..b41ea1a3aaf 100644 --- a/actions/setup/js/log_parser_bootstrap.test.cjs +++ b/actions/setup/js/log_parser_bootstrap.test.cjs @@ -291,7 +291,7 @@ describe("log_parser_bootstrap.cjs", () => { const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-")); const logFile = path.join(tmpDir, "test.log"); try { - fs.writeFileSync(logFile, `[claude-harness] attempt 1 failed: exitCode=1 isRateLimitError=true hasOutput=false\n[claude-harness] done: exitCode=1\nAPI Error: Request rejected (429)`); + fs.writeFileSync(logFile, `[claude-harness] attempt 1 failed: exitCode=1 hasOutput=false\n[claude-harness] done: exitCode=1\nAPI Error: Request rejected (429)`); process.env.GH_AW_AGENT_OUTPUT = logFile; delete process.env.GH_AW_SAFE_OUTPUTS; const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] }); From edf96e7dbfa095e1d3f98de8bd8d12dc273dea71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 02:14:41 +0000 Subject: [PATCH 10/14] fix: harden Claude startup diagnostics summaries Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 1 + actions/setup/js/claude_harness.test.cjs | 20 +++++- actions/setup/js/log_parser_bootstrap.cjs | 22 ++++-- .../setup/js/log_parser_bootstrap.test.cjs | 71 +++++++++++++++++++ 4 files changed, 107 insertions(+), 7 deletions(-) diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index 12841af0af5..3ddf5f2daa7 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -554,6 +554,7 @@ async function main() { if (startupRetriesUsed < startupRetryLimit) { startupRetriesUsed++; useContinueOnRetry = false; + delay = initialDelayMs; // attempt is 0-based; logs are 1-based, so "next attempt" is +2. const nextAttemptNumber = attempt + 2; const totalAttempts = maxRetries + 1; diff --git a/actions/setup/js/claude_harness.test.cjs b/actions/setup/js/claude_harness.test.cjs index c9d09dbf35d..290bd136e22 100644 --- a/actions/setup/js/claude_harness.test.cjs +++ b/actions/setup/js/claude_harness.test.cjs @@ -31,7 +31,7 @@ function makeHarnessTempDir(name) { return fs.mkdtempSync(path.join(agentTempDir, name)); } -function runHarnessWithStub({ stubScript, prompt = "fix the bug", extraArgs = [] }) { +function runHarnessWithStub({ stubScript, prompt = "fix the bug", extraArgs = [], extraEnv = {} }) { const tempDir = makeHarnessTempDir("claude-harness-"); const stubPath = path.join(tempDir, "stub.cjs"); const promptPath = path.join(tempDir, "prompt.txt"); @@ -41,7 +41,7 @@ function runHarnessWithStub({ stubScript, prompt = "fix the bug", extraArgs = [] const result = spawnSync(process.execPath, ["claude_harness.cjs", process.execPath, stubPath, "--print", ...extraArgs, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./claude_harness.cjs")), - env: { ...process.env, CLAUDE_HARNESS_STUB_CALLS: callsPath }, + env: { ...process.env, ...extraEnv, CLAUDE_HARNESS_STUB_CALLS: callsPath }, encoding: "utf8", timeout: 45000, }); @@ -467,6 +467,22 @@ process.exit(0); expect(result.stderr).toContain("no output produced — retrying startup as fresh run"); }); + it("does not retry no-output startup failure when GH_AW_CLAUDE_STARTUP_RETRIES=0", () => { + const stubScript = ` +const fs = require("fs"); +const callsPath = process.env.CLAUDE_HARNESS_STUB_CALLS; +fs.appendFileSync(callsPath, JSON.stringify({ args: process.argv.slice(2) }) + "\\n", "utf8"); +process.exit(1); +`; + const { result, calls } = runHarnessWithStub({ + stubScript, + extraEnv: { GH_AW_CLAUDE_STARTUP_RETRIES: "0" }, + }); + expect(result.status).not.toBe(0); + expect(calls.length).toBe(1); + expect(result.stderr).toContain("startup retry budget exhausted: 0/0"); + }); + it("returns true for normal partial-execution retry", () => { const result = shouldRetryWithContinue({ attempt: 0, diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index f9e3998612a..b4f9fd0e153 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -7,7 +7,7 @@ const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/i; const CLAUDE_RATE_LIMIT_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit/i; const CLAUDE_OVERLOAD_PATTERN = /overloaded_error|"overloaded"/i; -const CLAUDE_HTTP_5XX_STATUS_PATTERN = /(?:HTTP(?:\/\d\.\d)?\s+5\d{2}\b|status(?:\s+code)?\s*[:=]\s*5\d{2}\b|error(?:\s+code)?\s*[:=]?\s*5\d{2}\b)/i; +const CLAUDE_HTTP_5XX_STATUS_PATTERN = /(?:HTTP(?:\/\d\.\d)?\s+5\d{2}\b|status(?:\s+code)?\s*[:=]\s*5\d{2}\b|(?:http|fetch|request)\s+(?:failed|error)[^\n]*\b5\d{2}\b)/i; const STARTUP_DIAGNOSTIC_LINE_PATTERN = /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|\b5\d{2}\b|overloaded|inference)/i; const MAX_DIAGNOSTIC_TAIL_LINES = 8; @@ -30,9 +30,9 @@ function buildClaudeStartupDiagnostics(rawContent) { .map(line => line.trim()) .filter(Boolean); const startupLines = lines.filter(line => line.includes("[claude-harness]") || STARTUP_DIAGNOSTIC_LINE_PATTERN.test(line)); - const diagnosticLines = startupLines.length > 0 ? startupLines : lines; - const tailLines = diagnosticLines.slice(-MAX_DIAGNOSTIC_TAIL_LINES); + const tailLines = startupLines.slice(-MAX_DIAGNOSTIC_TAIL_LINES); const tailText = tailLines.join("\n"); + const safeTailText = escapeHtml(tailText); let exitCode = "unknown"; const donePrefix = "done: exitCode="; @@ -59,7 +59,7 @@ function buildClaudeStartupDiagnostics(rawContent) { const aiCreditsRateLimitError = CLAUDE_RATE_LIMIT_PATTERN.test(content) || CLAUDE_OVERLOAD_PATTERN.test(content); const transientInferenceAvailabilityError = aiCreditsRateLimitError || CLAUDE_HTTP_5XX_STATUS_PATTERN.test(content); const summaryLine = `Claude startup failed before structured logging (exitCode=${exitCode}).`; - const summaryMarkdown = tailText ? `
Claude startup diagnostics\n\n\`\`\`text\n${tailText}\n\`\`\`\n
` : ""; + const summaryMarkdown = safeTailText ? `
Claude startup diagnostics\n\n
${safeTailText}
\n
` : ""; return { exitCode, @@ -71,6 +71,14 @@ function buildClaudeStartupDiagnostics(rawContent) { }; } +/** + * @param {string} text + * @returns {string} + */ +function escapeHtml(text) { + return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); +} + /** * Bootstrap helper for log parser entry points. * Handles common logic for environment variable lookup, file existence checks, @@ -423,7 +431,11 @@ async function runLogParser(options) { } const errorCode = diagnostics.inferenceAccessError || diagnostics.transientInferenceAvailabilityError ? ERR_API : ERR_CONFIG; - const failureKind = diagnostics.inferenceAccessError || diagnostics.transientInferenceAvailabilityError ? "transient inference availability signal detected" : "startup/configuration failure detected"; + const failureKind = diagnostics.inferenceAccessError + ? "inference access denied by policy" + : diagnostics.transientInferenceAvailabilityError + ? "transient inference availability signal detected" + : "startup/configuration failure detected"; core.setFailed(`${errorCode}: Claude execution failed: no structured log entries were produced. ${diagnostics.summaryLine} ${failureKind}.`); } diff --git a/actions/setup/js/log_parser_bootstrap.test.cjs b/actions/setup/js/log_parser_bootstrap.test.cjs index b41ea1a3aaf..1332b415d89 100644 --- a/actions/setup/js/log_parser_bootstrap.test.cjs +++ b/actions/setup/js/log_parser_bootstrap.test.cjs @@ -247,6 +247,41 @@ describe("log_parser_bootstrap.cjs", () => { fs.rmdirSync(tmpDir); } }), + it("should omit Claude startup diagnostics tail when no startup lines are matched", async () => { + const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-")); + const logFile = path.join(tmpDir, "test.log"); + try { + fs.writeFileSync(logFile, "secret-token-value\nplain stdout line"); + process.env.GH_AW_AGENT_OUTPUT = logFile; + delete process.env.GH_AW_SAFE_OUTPUTS; + const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] }); + await runLogParser({ parseLog: mockParseLog, parserName: "Claude" }); + expect(mockCore.summary.addRaw).toHaveBeenCalledTimes(1); + expect(mockCore.summary.addRaw.mock.calls.flat().join("\n")).not.toContain("secret-token-value"); + } finally { + fs.unlinkSync(logFile); + delete process.env.GH_AW_AGENT_OUTPUT; + fs.rmdirSync(tmpDir); + } + }), + it("should escape Claude startup diagnostics summary content", async () => { + const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-")); + const logFile = path.join(tmpDir, "test.log"); + try { + fs.writeFileSync(logFile, "[claude-harness] attempt 1 failed: exitCode=1 hasOutput=false\n[claude-harness] stderr:
&```
\n[claude-harness] done: exitCode=1"); + process.env.GH_AW_AGENT_OUTPUT = logFile; + delete process.env.GH_AW_SAFE_OUTPUTS; + const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] }); + await runLogParser({ parseLog: mockParseLog, parserName: "Claude" }); + const diagnosticsSummary = mockCore.summary.addRaw.mock.calls[1][0]; + expect(diagnosticsSummary).toContain("
");
+            expect(diagnosticsSummary).toContain("<pre>&```</pre>");
+          } finally {
+            fs.unlinkSync(logFile);
+            delete process.env.GH_AW_AGENT_OUTPUT;
+            fs.rmdirSync(tmpDir);
+          }
+        }),
         it("should not print 'parsed successfully' for Claude when logEntries is empty", async () => {
           const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-"));
           const logFile = path.join(tmpDir, "test.log");
@@ -306,6 +341,42 @@ describe("log_parser_bootstrap.cjs", () => {
             fs.rmdirSync(tmpDir);
           }
         }),
+        it("should classify Claude empty-log startup inference-access denials separately", async () => {
+          const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-"));
+          const logFile = path.join(tmpDir, "test.log");
+          try {
+            fs.writeFileSync(logFile, `[claude-harness] attempt 1 failed: exitCode=1 hasOutput=false\nAccess denied by policy settings\n[claude-harness] done: exitCode=1`);
+            process.env.GH_AW_AGENT_OUTPUT = logFile;
+            delete process.env.GH_AW_SAFE_OUTPUTS;
+            const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] });
+            await runLogParser({ parseLog: mockParseLog, parserName: "Claude" });
+            expect(mockCore.setFailed).toHaveBeenCalledWith(`${ERR_API}: Claude execution failed: no structured log entries were produced. Claude startup failed before structured logging (exitCode=1). inference access denied by policy.`);
+            expect(mockCore.setOutput).toHaveBeenCalledWith("inference_access_error", "true");
+            expect(mockCore.setOutput).not.toHaveBeenCalledWith("ai_credits_rate_limit_error", "true");
+          } finally {
+            fs.unlinkSync(logFile);
+            delete process.env.GH_AW_AGENT_OUTPUT;
+            fs.rmdirSync(tmpDir);
+          }
+        }),
+        it("should not classify non-HTTP 500 text as transient inference availability", async () => {
+          const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-"));
+          const logFile = path.join(tmpDir, "test.log");
+          try {
+            fs.writeFileSync(logFile, `[claude-harness] attempt 1 failed: exitCode=1 hasOutput=false\nerror code 500\n[claude-harness] done: exitCode=1`);
+            process.env.GH_AW_AGENT_OUTPUT = logFile;
+            delete process.env.GH_AW_SAFE_OUTPUTS;
+            const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] });
+            await runLogParser({ parseLog: mockParseLog, parserName: "Claude" });
+            expect(mockCore.setFailed).toHaveBeenCalledWith(
+              `${ERR_CONFIG}: Claude execution failed: no structured log entries were produced. Claude startup failed before structured logging (exitCode=1). startup/configuration failure detected.`
+            );
+          } finally {
+            fs.unlinkSync(logFile);
+            delete process.env.GH_AW_AGENT_OUTPUT;
+            fs.rmdirSync(tmpDir);
+          }
+        }),
         it("should treat logEntries: null as missing entries for Claude guardrail (safe outputs → warning)", async () => {
           const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-"));
           const logFile = path.join(tmpDir, "test.log");

From 12828fbfbca3bbfe904d53ca932be03f93305cc6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 9 Jul 2026 02:18:13 +0000
Subject: [PATCH 11/14] test: cover Claude startup diagnostics edge cases

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
 actions/setup/js/log_parser_bootstrap.cjs      | 2 +-
 actions/setup/js/log_parser_bootstrap.test.cjs | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs
index b4f9fd0e153..c328ca687fb 100644
--- a/actions/setup/js/log_parser_bootstrap.cjs
+++ b/actions/setup/js/log_parser_bootstrap.cjs
@@ -7,7 +7,7 @@ const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs");
 const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/i;
 const CLAUDE_RATE_LIMIT_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit/i;
 const CLAUDE_OVERLOAD_PATTERN = /overloaded_error|"overloaded"/i;
-const CLAUDE_HTTP_5XX_STATUS_PATTERN = /(?:HTTP(?:\/\d\.\d)?\s+5\d{2}\b|status(?:\s+code)?\s*[:=]\s*5\d{2}\b|(?:http|fetch|request)\s+(?:failed|error)[^\n]*\b5\d{2}\b)/i;
+const CLAUDE_HTTP_5XX_STATUS_PATTERN = /(?:HTTP(?:\/\d\.\d)?\s+5\d{2}\b|status(?:\s+code)?\s*[:=]\s*5\d{2}\b|(?:http|fetch|request)\s+(?:failed|error)[^\n]*?\b5\d{2}\b)/i;
 const STARTUP_DIAGNOSTIC_LINE_PATTERN = /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|\b5\d{2}\b|overloaded|inference)/i;
 const MAX_DIAGNOSTIC_TAIL_LINES = 8;
 
diff --git a/actions/setup/js/log_parser_bootstrap.test.cjs b/actions/setup/js/log_parser_bootstrap.test.cjs
index 1332b415d89..b860c6fdfa2 100644
--- a/actions/setup/js/log_parser_bootstrap.test.cjs
+++ b/actions/setup/js/log_parser_bootstrap.test.cjs
@@ -267,8 +267,9 @@ describe("log_parser_bootstrap.cjs", () => {
         it("should escape Claude startup diagnostics summary content", async () => {
           const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-"));
           const logFile = path.join(tmpDir, "test.log");
+          const escapedTailLog = "[claude-harness] attempt 1 failed: exitCode=1 hasOutput=false\n[claude-harness] stderr: 
&```
\n[claude-harness] done: exitCode=1"; try { - fs.writeFileSync(logFile, "[claude-harness] attempt 1 failed: exitCode=1 hasOutput=false\n[claude-harness] stderr:
&```
\n[claude-harness] done: exitCode=1"); + fs.writeFileSync(logFile, escapedTailLog); process.env.GH_AW_AGENT_OUTPUT = logFile; delete process.env.GH_AW_SAFE_OUTPUTS; const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] }); From a120adcadf3de7454fc47e154cc148e71505cd8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 02:21:43 +0000 Subject: [PATCH 12/14] chore: tighten Claude diagnostics matching and escaping Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/log_parser_bootstrap.cjs | 6 +++++- actions/setup/js/log_parser_bootstrap.test.cjs | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index c328ca687fb..2b94b184d09 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -7,6 +7,10 @@ const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/i; const CLAUDE_RATE_LIMIT_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit/i; const CLAUDE_OVERLOAD_PATTERN = /overloaded_error|"overloaded"/i; +// Treat only clearly HTTP-scoped 5xx strings as transient inference availability: +// - protocol/status lines such as "HTTP/1.1 503" +// - structured status-code fields such as "status code: 502" +// - request/fetch/http error prose that includes a 5xx code const CLAUDE_HTTP_5XX_STATUS_PATTERN = /(?:HTTP(?:\/\d\.\d)?\s+5\d{2}\b|status(?:\s+code)?\s*[:=]\s*5\d{2}\b|(?:http|fetch|request)\s+(?:failed|error)[^\n]*?\b5\d{2}\b)/i; const STARTUP_DIAGNOSTIC_LINE_PATTERN = /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|\b5\d{2}\b|overloaded|inference)/i; const MAX_DIAGNOSTIC_TAIL_LINES = 8; @@ -76,7 +80,7 @@ function buildClaudeStartupDiagnostics(rawContent) { * @returns {string} */ function escapeHtml(text) { - return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); + return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); } /** diff --git a/actions/setup/js/log_parser_bootstrap.test.cjs b/actions/setup/js/log_parser_bootstrap.test.cjs index b860c6fdfa2..916a5f38b48 100644 --- a/actions/setup/js/log_parser_bootstrap.test.cjs +++ b/actions/setup/js/log_parser_bootstrap.test.cjs @@ -267,9 +267,9 @@ describe("log_parser_bootstrap.cjs", () => { it("should escape Claude startup diagnostics summary content", async () => { const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-")); const logFile = path.join(tmpDir, "test.log"); - const escapedTailLog = "[claude-harness] attempt 1 failed: exitCode=1 hasOutput=false\n[claude-harness] stderr:
&```
\n[claude-harness] done: exitCode=1"; + const rawTailLogWithHtml = "[claude-harness] attempt 1 failed: exitCode=1 hasOutput=false\n[claude-harness] stderr:
&```
\n[claude-harness] done: exitCode=1"; try { - fs.writeFileSync(logFile, escapedTailLog); + fs.writeFileSync(logFile, rawTailLogWithHtml); process.env.GH_AW_AGENT_OUTPUT = logFile; delete process.env.GH_AW_SAFE_OUTPUTS; const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] }); From dafe82eb4bb33ba508f06075d20ce9eba3c78c2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 02:25:29 +0000 Subject: [PATCH 13/14] refactor: clarify Claude startup diagnostics matching Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/claude_harness.test.cjs | 3 ++- actions/setup/js/log_parser_bootstrap.cjs | 12 +++++++----- actions/setup/js/log_parser_bootstrap.test.cjs | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/actions/setup/js/claude_harness.test.cjs b/actions/setup/js/claude_harness.test.cjs index 290bd136e22..c0767aee072 100644 --- a/actions/setup/js/claude_harness.test.cjs +++ b/actions/setup/js/claude_harness.test.cjs @@ -478,8 +478,9 @@ process.exit(1); stubScript, extraEnv: { GH_AW_CLAUDE_STARTUP_RETRIES: "0" }, }); - expect(result.status).not.toBe(0); + expect(result.status).toBe(1); expect(calls.length).toBe(1); + expect(calls[0].args).toContain("fix the bug"); expect(result.stderr).toContain("startup retry budget exhausted: 0/0"); }); diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index 2b94b184d09..133604f52df 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -7,11 +7,10 @@ const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/i; const CLAUDE_RATE_LIMIT_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit/i; const CLAUDE_OVERLOAD_PATTERN = /overloaded_error|"overloaded"/i; -// Treat only clearly HTTP-scoped 5xx strings as transient inference availability: -// - protocol/status lines such as "HTTP/1.1 503" -// - structured status-code fields such as "status code: 502" -// - request/fetch/http error prose that includes a 5xx code -const CLAUDE_HTTP_5XX_STATUS_PATTERN = /(?:HTTP(?:\/\d\.\d)?\s+5\d{2}\b|status(?:\s+code)?\s*[:=]\s*5\d{2}\b|(?:http|fetch|request)\s+(?:failed|error)[^\n]*?\b5\d{2}\b)/i; +const CLAUDE_HTTP_5XX_PROTOCOL_PATTERN = /HTTP(?:\/\d\.\d)?\s+5\d{2}\b/; +const CLAUDE_HTTP_5XX_STATUS_FIELD_PATTERN = /status(?:\s+code)?\s*[:=]\s*5\d{2}\b/; +const CLAUDE_HTTP_5XX_REQUEST_ERROR_PATTERN = /(?:http|fetch|request)\s+(?:failed|error)[^\n]*?\b5\d{2}\b/; +const CLAUDE_HTTP_5XX_STATUS_PATTERN = new RegExp([CLAUDE_HTTP_5XX_PROTOCOL_PATTERN.source, CLAUDE_HTTP_5XX_STATUS_FIELD_PATTERN.source, CLAUDE_HTTP_5XX_REQUEST_ERROR_PATTERN.source].join("|"), "i"); const STARTUP_DIAGNOSTIC_LINE_PATTERN = /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|\b5\d{2}\b|overloaded|inference)/i; const MAX_DIAGNOSTIC_TAIL_LINES = 8; @@ -34,6 +33,9 @@ function buildClaudeStartupDiagnostics(rawContent) { .map(line => line.trim()) .filter(Boolean); const startupLines = lines.filter(line => line.includes("[claude-harness]") || STARTUP_DIAGNOSTIC_LINE_PATTERN.test(line)); + // Intentionally avoid falling back to arbitrary raw log lines here. For empty-structured-log + // failures, the unmatched tail can contain unrelated stdout/stderr, prompts, or secrets; when + // no startup/diagnostic lines are recognized, we emit no tail section rather than risk leaking it. const tailLines = startupLines.slice(-MAX_DIAGNOSTIC_TAIL_LINES); const tailText = tailLines.join("\n"); const safeTailText = escapeHtml(tailText); diff --git a/actions/setup/js/log_parser_bootstrap.test.cjs b/actions/setup/js/log_parser_bootstrap.test.cjs index 916a5f38b48..bd1d5ffb77b 100644 --- a/actions/setup/js/log_parser_bootstrap.test.cjs +++ b/actions/setup/js/log_parser_bootstrap.test.cjs @@ -267,9 +267,9 @@ describe("log_parser_bootstrap.cjs", () => { it("should escape Claude startup diagnostics summary content", async () => { const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-")); const logFile = path.join(tmpDir, "test.log"); - const rawTailLogWithHtml = "[claude-harness] attempt 1 failed: exitCode=1 hasOutput=false\n[claude-harness] stderr:
&```
\n[claude-harness] done: exitCode=1"; + const logContentWithHtmlChars = "[claude-harness] attempt 1 failed: exitCode=1 hasOutput=false\n[claude-harness] stderr:
&```
\n[claude-harness] done: exitCode=1"; try { - fs.writeFileSync(logFile, rawTailLogWithHtml); + fs.writeFileSync(logFile, logContentWithHtmlChars); process.env.GH_AW_AGENT_OUTPUT = logFile; delete process.env.GH_AW_SAFE_OUTPUTS; const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] }); From 833c290ae74c6549d69510d7c0dd1ba4fae6dbc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 02:29:00 +0000 Subject: [PATCH 14/14] chore: document Claude startup 5xx matchers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/log_parser_bootstrap.cjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index 133604f52df..32c60de6965 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -7,8 +7,11 @@ const { ERR_API, ERR_CONFIG, ERR_VALIDATION } = require("./error_codes.cjs"); const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/i; const CLAUDE_RATE_LIMIT_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit/i; const CLAUDE_OVERLOAD_PATTERN = /overloaded_error|"overloaded"/i; +/** Matches HTTP protocol/status lines such as "HTTP/1.1 503". */ const CLAUDE_HTTP_5XX_PROTOCOL_PATTERN = /HTTP(?:\/\d\.\d)?\s+5\d{2}\b/; +/** Matches structured status fields such as "status: 502" or "status code=500". */ const CLAUDE_HTTP_5XX_STATUS_FIELD_PATTERN = /status(?:\s+code)?\s*[:=]\s*5\d{2}\b/; +/** Matches request/fetch/http error prose that carries an explicit 5xx code. */ const CLAUDE_HTTP_5XX_REQUEST_ERROR_PATTERN = /(?:http|fetch|request)\s+(?:failed|error)[^\n]*?\b5\d{2}\b/; const CLAUDE_HTTP_5XX_STATUS_PATTERN = new RegExp([CLAUDE_HTTP_5XX_PROTOCOL_PATTERN.source, CLAUDE_HTTP_5XX_STATUS_FIELD_PATTERN.source, CLAUDE_HTTP_5XX_REQUEST_ERROR_PATTERN.source].join("|"), "i"); const STARTUP_DIAGNOSTIC_LINE_PATTERN = /(?:ERR_|Error:|CAPIError|Authentication failed|rate[_ -]?limit|429|\b5\d{2}\b|overloaded|inference)/i; @@ -82,7 +85,8 @@ function buildClaudeStartupDiagnostics(rawContent) { * @returns {string} */ function escapeHtml(text) { - return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); + const htmlEntities = { '"': """, "'": "'", "&": "&", "<": "<", ">": ">" }; + return text.replace(/["'&<>]/g, char => htmlEntities[char]); } /**