diff --git a/.github/workflows/design-decision-gate.lock.yml b/.github/workflows/design-decision-gate.lock.yml index 11095f46bfc..7cc34f5a918 100644 --- a/.github/workflows/design-decision-gate.lock.yml +++ b/.github/workflows/design-decision-gate.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"38898cde64f51b20369b229fc09ae1cfe5256826401cea1de94ac098c4ebb6bc","body_hash":"9c75d193ef9e992935f5d5915c86cb21b5fa18fbbbf0354d584c0bf7bdf76410","strict":true,"agent_id":"claude","agent_model":"claude-sonnet-4-6","engine_versions":{"claude":"2.1.233"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e9d71c9a51b4fd95ba4ebce2fd42e801f93bfaeb1bab050640c8e47c54b4a0c7","body_hash":"328cfc5ada7f0fc855687b3c6462e14095f36c7f4b2aa67e0902a87a236eb33f","strict":true,"agent_id":"claude","agent_model":"claude-sonnet-4-6","engine_versions":{"claude":"2.1.233"}} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.1","digest":"sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.1@sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}],"has_pull_request":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -564,7 +564,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 \"::notice::workflow_dispatch did not include inputs.pr_number; skipping ADR gate.\"\n mkdir -p /tmp/gh-aw/agent\n printf '{\"number\":null,\"title\":\"No pull request provided\",\"body\":\"\",\"labels\":[],\"baseRefName\":\"\",\"headRefName\":\"\",\"author\":null,\"url\":\"\"}\\n' \\\n > /tmp/gh-aw/agent/pr.json\n printf '[]\\n' > /tmp/gh-aw/agent/pr-files.json\n printf '# ADR gate skipped: workflow_dispatch did not include inputs.pr_number.\\n' \\\n > /tmp/gh-aw/agent/pr.diff\n printf 'No .design-gate.yml read because workflow_dispatch did not include inputs.pr_number.\\n' \\\n > /tmp/gh-aw/agent/design-gate-config.yml\n jq -n \\\n --arg skip_reason \"workflow_dispatch did not include inputs.pr_number\" \\\n '{\n pr_number: null,\n threshold: 100,\n has_custom_config: false,\n has_implementation_label: false,\n default_business_additions: 0,\n requires_adr_by_default_volume: false,\n file_count: 0,\n diff_available: false,\n skip_reason: $skip_reason\n }' > /tmp/gh-aw/agent/adr-prefetch-summary.json\n exit 0\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" + run: "set -euo pipefail\n\nif [ \"$EXPR_GITHUB_EVENT_NAME\" = \"workflow_dispatch\" ] && [ -z \"${PR_NUMBER:-}\" ]; then\n echo \"::notice::workflow_dispatch did not include inputs.pr_number; skipping ADR gate.\"\n mkdir -p /tmp/gh-aw/agent\n printf '{\"number\":null,\"title\":\"No pull request provided\",\"body\":\"\",\"labels\":[],\"baseRefName\":\"\",\"headRefName\":\"\",\"author\":null,\"url\":\"\"}\\n' \\\n > /tmp/gh-aw/agent/pr.json\n printf '[]\\n' > /tmp/gh-aw/agent/pr-files.json\n printf '# ADR gate skipped: workflow_dispatch did not include inputs.pr_number.\\n' \\\n > /tmp/gh-aw/agent/pr.diff\n printf 'No .design-gate.yml read because workflow_dispatch did not include inputs.pr_number.\\n' \\\n > /tmp/gh-aw/agent/design-gate-config.yml\n jq -n \\\n --arg skip_reason \"workflow_dispatch did not include inputs.pr_number\" \\\n '{\n pr_number: null,\n threshold: 100,\n has_custom_config: false,\n has_implementation_label: false,\n default_business_additions: 0,\n requires_adr_by_default_volume: false,\n file_count: 0,\n diff_size_bytes: 0,\n diff_truncated: false,\n diff_available: false,\n skip_reason: $skip_reason\n }' > /tmp/gh-aw/agent/adr-prefetch-summary.json\n exit 0\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-full.json\n\nFILE_COUNT=$(jq 'length' /tmp/gh-aw/agent/pr-files-full.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\n DIFF_SIZE_BYTES=0\n DIFF_TRUNCATED=true\nelse\n MAX_DIFF_BYTES=200000\n jq -r '.[] | \"diff --git a/\\(.filename) b/\\(.filename)\\n--- a/\\(.filename)\\n+++ b/\\(.filename)\\n\\(.patch // \"# Patch unavailable\")\\n\"' \\\n /tmp/gh-aw/agent/pr-files-full.json > /tmp/gh-aw/agent/pr.diff.full\n DIFF_SIZE_BYTES=$(wc -c < /tmp/gh-aw/agent/pr.diff.full)\n if [ \"$DIFF_SIZE_BYTES\" -gt \"$MAX_DIFF_BYTES\" ]; then\n head -c \"$MAX_DIFF_BYTES\" /tmp/gh-aw/agent/pr.diff.full > /tmp/gh-aw/agent/pr.diff\n printf '\\n# Diff truncated at %s bytes; use pr-files.json for file metadata instead.\\n' \"$MAX_DIFF_BYTES\" \\\n >> /tmp/gh-aw/agent/pr.diff\n rm /tmp/gh-aw/agent/pr.diff.full\n DIFF_TRUNCATED=true\n else\n mv /tmp/gh-aw/agent/pr.diff.full /tmp/gh-aw/agent/pr.diff\n DIFF_TRUNCATED=false\n fi\nfi\njq '[.[] | del(.patch)]' /tmp/gh-aw/agent/pr-files-full.json > /tmp/gh-aw/agent/pr-files.json\nrm /tmp/gh-aw/agent/pr-files-full.json\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_size_bytes \"$DIFF_SIZE_BYTES\" \\\n --argjson diff_truncated \"$DIFF_TRUNCATED\" \\\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_size_bytes: $diff_size_bytes,\n diff_truncated: $diff_truncated,\n diff_available: $diff_available\n }' > /tmp/gh-aw/agent/adr-prefetch-summary.json" - name: Configure Git credentials env: @@ -1066,6 +1066,8 @@ jobs: DISABLE_BUG_COMMAND: 1 DISABLE_ERROR_REPORTING: 1 DISABLE_TELEMETRY: 1 + GH_AW_HARNESS_STALL_ERROR: true + GH_AW_HARNESS_STALL_WARNING_MS: 60000 GH_AW_LLM_PROVIDER: anthropic GH_AW_MAX_TURNS: 30 GH_AW_MCP_CONFIG: ${{ runner.temp }}/gh-aw/mcp-config/mcp-servers.json @@ -1706,6 +1708,8 @@ jobs: DISABLE_BUG_COMMAND: 1 DISABLE_ERROR_REPORTING: 1 DISABLE_TELEMETRY: 1 + GH_AW_HARNESS_STALL_ERROR: true + GH_AW_HARNESS_STALL_WARNING_MS: 60000 GH_AW_LLM_PROVIDER: anthropic GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection @@ -1943,6 +1947,8 @@ jobs: DISABLE_BUG_COMMAND: 1 DISABLE_ERROR_REPORTING: 1 DISABLE_TELEMETRY: 1 + GH_AW_HARNESS_STALL_ERROR: true + GH_AW_HARNESS_STALL_WARNING_MS: 60000 GH_AW_LLM_PROVIDER: anthropic GH_AW_MAX_TURNS: 30 GH_AW_PHASE: evals diff --git a/.github/workflows/design-decision-gate.md b/.github/workflows/design-decision-gate.md index 52c6be43c16..d93a8b305b7 100644 --- a/.github/workflows/design-decision-gate.md +++ b/.github/workflows/design-decision-gate.md @@ -34,6 +34,9 @@ max-turns: 30 model: claude-sonnet-4-6 engine: id: claude + env: + GH_AW_HARNESS_STALL_WARNING_MS: "60000" + GH_AW_HARNESS_STALL_ERROR: "true" safe-outputs: add-comment: max: 2 @@ -109,6 +112,8 @@ steps: default_business_additions: 0, requires_adr_by_default_volume: false, file_count: 0, + diff_size_bytes: 0, + diff_truncated: false, diff_available: false, skip_reason: $skip_reason }' > /tmp/gh-aw/agent/adr-prefetch-summary.json @@ -123,19 +128,34 @@ steps: > /tmp/gh-aw/agent/pr.json gh api --paginate "repos/$EXPR_GITHUB_REPOSITORY/pulls/$PR_NUMBER/files?per_page=100" \ - --jq '.[]' | jq -s '.' > /tmp/gh-aw/agent/pr-files.json + --jq '.[]' | jq -s '.' > /tmp/gh-aw/agent/pr-files-full.json - FILE_COUNT=$(jq 'length' /tmp/gh-aw/agent/pr-files.json) + FILE_COUNT=$(jq 'length' /tmp/gh-aw/agent/pr-files-full.json) if [ "$FILE_COUNT" -gt 300 ]; then 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." 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" \ > /tmp/gh-aw/agent/pr.diff + DIFF_SIZE_BYTES=0 + DIFF_TRUNCATED=true else - gh pr diff "$PR_NUMBER" \ - --repo "$EXPR_GITHUB_REPOSITORY" \ - > /tmp/gh-aw/agent/pr.diff + MAX_DIFF_BYTES=200000 + jq -r '.[] | "diff --git a/\(.filename) b/\(.filename)\n--- a/\(.filename)\n+++ b/\(.filename)\n\(.patch // "# Patch unavailable")\n"' \ + /tmp/gh-aw/agent/pr-files-full.json > /tmp/gh-aw/agent/pr.diff.full + DIFF_SIZE_BYTES=$(wc -c < /tmp/gh-aw/agent/pr.diff.full) + if [ "$DIFF_SIZE_BYTES" -gt "$MAX_DIFF_BYTES" ]; then + head -c "$MAX_DIFF_BYTES" /tmp/gh-aw/agent/pr.diff.full > /tmp/gh-aw/agent/pr.diff + printf '\n# Diff truncated at %s bytes; use pr-files.json for file metadata instead.\n' "$MAX_DIFF_BYTES" \ + >> /tmp/gh-aw/agent/pr.diff + rm /tmp/gh-aw/agent/pr.diff.full + DIFF_TRUNCATED=true + else + mv /tmp/gh-aw/agent/pr.diff.full /tmp/gh-aw/agent/pr.diff + DIFF_TRUNCATED=false + fi fi + jq '[.[] | del(.patch)]' /tmp/gh-aw/agent/pr-files-full.json > /tmp/gh-aw/agent/pr-files.json + rm /tmp/gh-aw/agent/pr-files-full.json if [ -f "$EXPR_GITHUB_WORKSPACE/.design-gate.yml" ]; then cp "$EXPR_GITHUB_WORKSPACE/.design-gate.yml" /tmp/gh-aw/agent/design-gate-config.yml @@ -155,6 +175,8 @@ steps: --arg pr_number "$PR_NUMBER" \ --arg threshold "100" \ --argjson file_count "$FILE_COUNT" \ + --argjson diff_size_bytes "$DIFF_SIZE_BYTES" \ + --argjson diff_truncated "$DIFF_TRUNCATED" \ --argjson diff_available "$(jq -n --argjson fc "$FILE_COUNT" 'if $fc <= 300 then true else false end')" \ '{ pr_number: ($pr_number | tonumber), @@ -164,6 +186,8 @@ steps: default_business_additions: $default_business_additions, requires_adr_by_default_volume: ($default_business_additions > ($threshold | tonumber)), file_count: $file_count, + diff_size_bytes: $diff_size_bytes, + diff_truncated: $diff_truncated, diff_available: $diff_available }' > /tmp/gh-aw/agent/adr-prefetch-summary.json evals: @@ -224,11 +248,12 @@ Stop and emit a safe output **immediately** when any of the following is true: 2. If a pre-fetched file is missing or returns a permission error, fall back to the equivalent GitHub MCP tool immediately (do not retry the file read): - Missing `pr.json` → `mcp__github__get_pull_request` - Missing `pr-files.json` → `mcp__github__get_pull_request_files` - - Missing `pr.diff` → `mcp__github__get_pull_request_diff` (only if `diff_available` is `true` in the summary; if `false`, the diff exceeds the 300-file API limit — use `pr-files.json` instead and do **not** call the diff API) + - Missing `pr.diff` → `mcp__github__get_pull_request_diff` (only if `diff_available` is `true` and `diff_truncated` is `false` in the summary; otherwise use `pr-files.json` and do **not** call the diff API) - Missing `adr-prefetch-summary.json` → compute manually from PR files and labels 3. Do **not** perform broad exploration. Only fetch extra data if a required field is missing from pre-fetched files. -4. Call exactly one final safe output action (`add-comment`, `push-to-pull-request-branch`, or `noop`) and then stop. -5. If you have enough evidence to decide, stop immediately. Do not gather optional data. +4. `pr.diff` is capped at 200,000 bytes. If `diff_truncated` is `true`, do not read it; use `pr-files.json` for file metadata and inspect only the specific changed files needed for the ADR decision. +5. Call exactly one final safe output action (`add-comment`, `push-to-pull-request-branch`, or `noop`) and then stop. +6. If you have enough evidence to decide, stop immediately. Do not gather optional data. ## Gate Quality Bar diff --git a/actions/setup/js/process_runner.cjs b/actions/setup/js/process_runner.cjs index cc6ee4ff95f..dfb00736704 100644 --- a/actions/setup/js/process_runner.cjs +++ b/actions/setup/js/process_runner.cjs @@ -139,6 +139,7 @@ function runProcess({ command, args, attempt, log, logArgs, env, postResultWatch /** @type {NodeJS.Timeout | null} */ let stallWatchdogTimer = null; const stallIntervalMs = Number.isFinite(Number(stallWarningIntervalMs)) ? Math.max(0, Number(stallWarningIntervalMs)) : resolveStallWarningIntervalMs(env ?? process.env); + const stallIsError = (env ?? process.env).GH_AW_HARNESS_STALL_ERROR === "true"; let stallWarnings = 0; let stalledSinceMs = 0; @@ -188,7 +189,8 @@ function runProcess({ command, args, attempt, log, logArgs, env, postResultWatch if (stalledSinceMs === 0) stalledSinceMs = lastActivityAt; stallWarnings++; log( - `attempt ${attempt + 1}: stall watchdog: no output from '${command}' for ${formatDuration(idleMs)}` + + (stallIsError ? "::error::Agent CLI exceeded its no-output budget. " : "") + + `attempt ${attempt + 1}: stall watchdog: no output from '${command}' for ${formatDuration(idleMs)}` + ` (elapsed=${formatDuration(Date.now() - startTime)} pid=${child.pid ?? "unknown"} warnings=${stallWarnings})` + ` - the step may be hung${formatStepTimeoutBudget(startTime, env ?? process.env)}` ); diff --git a/actions/setup/js/process_runner.test.cjs b/actions/setup/js/process_runner.test.cjs index df74926236e..cfa6534e642 100644 --- a/actions/setup/js/process_runner.test.cjs +++ b/actions/setup/js/process_runner.test.cjs @@ -518,6 +518,19 @@ describe("process_runner.cjs", () => { expect(logs.some(line => line.includes("stall watchdog: output resumed after"))).toBe(true); }); + it("emits an error annotation when configured for a stalled agent CLI", async () => { + const logs = []; + await runProcess({ + command: process.execPath, + args: ["-e", "setTimeout(() => process.exit(0), 300);"], + attempt: 0, + log: msg => logs.push(msg), + env: { GH_AW_HARNESS_STALL_ERROR: "true" }, + stallWarningIntervalMs: 100, + }); + expect(logs.some(line => line.startsWith("::error::Agent CLI exceeded its no-output budget."))).toBe(true); + }); + it("does not log stall warnings when the process keeps producing output", async () => { const logs = []; await runProcess({ diff --git a/docs/src/content/docs/reference/environment-variables.md b/docs/src/content/docs/reference/environment-variables.md index 61056450f47..011dd3d491a 100644 --- a/docs/src/content/docs/reference/environment-variables.md +++ b/docs/src/content/docs/reference/environment-variables.md @@ -177,6 +177,8 @@ Unlike the post-result watchdog, the stall watchdog never terminates the agent p The warning repeats on each interval while the silence continues, and a `stall watchdog: output resumed after ...` line is logged once output comes back. This makes a hung step diagnosable from the step log alone, without cross-referencing job or step metadata. The final `process closed` line reports `stallWarnings=` when any warning fired. +Set `GH_AW_HARNESS_STALL_ERROR: "true"` to emit each configured stall warning as a GitHub Actions error annotation. + ### Engine-specific harness settings | Variable | Engine | Default | Units / range | Description |