From 6b84564b4250bded9a595476914216b74664ec52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 20:30:01 +0000 Subject: [PATCH 1/6] Initial plan From 648ce5a382d9498bf0d28b4a7eccb939036c2100 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 20:42:31 +0000 Subject: [PATCH 2/6] feat: add experiments prompt_style A/B test to daily-community-attribution workflow - Add `experiments: prompt_style: [concise, verbose]` to frontmatter - Wrap Sections 1-6 and Token Budget Guidelines with concise/verbose conditional blocks using `{{#if (eq experiments.prompt_style "concise")}}` - Regenerate lock file via `gh aw compile` Closes # Agent-Logs-Url: https://github.com/github/gh-aw/sessions/fc6233bc-38b0-43b7-9e11-4a0a345419b3 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../daily-community-attribution.lock.yml | 79 +++++++++++++++---- .../workflows/daily-community-attribution.md | 65 ++++++++++++++- 2 files changed, 127 insertions(+), 17 deletions(-) diff --git a/.github/workflows/daily-community-attribution.lock.yml b/.github/workflows/daily-community-attribution.lock.yml index df17348ef27..da95ba051eb 100644 --- a/.github/workflows/daily-community-attribution.lock.yml +++ b/.github/workflows/daily-community-attribution.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"13e40f3f2892cf4ca876974bfee05f955c4d0c6a10076406d4703e34fee650fe","strict":true,"agent_id":"copilot","agent_model":"claude-haiku-4.5"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.29","digest":"sha256:e68f37e36962dcb3f3d1de680a49bc2302cefd001b941a7dc377155ec7ce42f4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.29@sha256:e68f37e36962dcb3f3d1de680a49bc2302cefd001b941a7dc377155ec7ce42f4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.29","digest":"sha256:d1219e4110684402aabbeb5a43858f26790c9d0be210581cf3f7a521bd2c87b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.29@sha256:d1219e4110684402aabbeb5a43858f26790c9d0be210581cf3f7a521bd2c87b6"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.29","digest":"sha256:8a71ad9e40454051672312917e51567abfb8251d7c294d086c48f63d84e4cb53","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.29@sha256:8a71ad9e40454051672312917e51567abfb8251d7c294d086c48f63d84e4cb53"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.3"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"4e00acbfe838ba73e585cd18532b73db56b9c85e6815e2657950cc57a34848fc","strict":true,"agent_id":"copilot","agent_model":"claude-haiku-4.5"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.29","digest":"sha256:e68f37e36962dcb3f3d1de680a49bc2302cefd001b941a7dc377155ec7ce42f4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.29@sha256:e68f37e36962dcb3f3d1de680a49bc2302cefd001b941a7dc377155ec7ce42f4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.29","digest":"sha256:d1219e4110684402aabbeb5a43858f26790c9d0be210581cf3f7a521bd2c87b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.29@sha256:d1219e4110684402aabbeb5a43858f26790c9d0be210581cf3f7a521bd2c87b6"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.29","digest":"sha256:8a71ad9e40454051672312917e51567abfb8251d7c294d086c48f63d84e4cb53","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.29@sha256:8a71ad9e40454051672312917e51567abfb8251d7c294d086c48f63d84e4cb53"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.3"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -40,6 +40,8 @@ # - GITHUB_TOKEN # # Custom actions used: +# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -89,6 +91,7 @@ jobs: comment_id: "" comment_repo: "" engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + experiments: ${{ steps.pick-experiment.outputs.experiments }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} @@ -173,10 +176,45 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); await main(); + - name: Restore experiment state + id: restore-experiment-cache + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: experiments-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + restore-keys: experiments-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}- + path: /tmp/gh-aw/experiments + - name: Pick experiment variants + id: pick-experiment + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_EXPERIMENT_SPEC: '{"prompt_style":["concise","verbose"]}' + GH_AW_EXPERIMENT_STATE_FILE: /tmp/gh-aw/experiments/state.json + GH_AW_EXPERIMENT_STATE_DIR: /tmp/gh-aw/experiments + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/pick_experiment.cjs'); + await main(); + - name: Save experiment state + if: always() + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: experiments-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/experiments + - name: Upload experiment artifact + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: experiment + path: /tmp/gh-aw/experiments + if-no-files-found: ignore + retention-days: 30 - name: Create prompt with built-in context env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPERIMENTS_PROMPT_STYLE: ${{ steps.pick-experiment.outputs.prompt_style }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} @@ -189,24 +227,24 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_9f0aa82a79e6ab0b_EOF' + cat << 'GH_AW_PROMPT_6fdda29ef119f2eb_EOF' - GH_AW_PROMPT_9f0aa82a79e6ab0b_EOF + GH_AW_PROMPT_6fdda29ef119f2eb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/repo_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_9f0aa82a79e6ab0b_EOF' + cat << 'GH_AW_PROMPT_6fdda29ef119f2eb_EOF' Tools: create_issue, create_pull_request, missing_tool, missing_data, noop - GH_AW_PROMPT_9f0aa82a79e6ab0b_EOF + GH_AW_PROMPT_6fdda29ef119f2eb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_9f0aa82a79e6ab0b_EOF' + cat << 'GH_AW_PROMPT_6fdda29ef119f2eb_EOF' - GH_AW_PROMPT_9f0aa82a79e6ab0b_EOF + GH_AW_PROMPT_6fdda29ef119f2eb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_9f0aa82a79e6ab0b_EOF' + cat << 'GH_AW_PROMPT_6fdda29ef119f2eb_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -235,20 +273,21 @@ jobs: {{/if}} - GH_AW_PROMPT_9f0aa82a79e6ab0b_EOF + GH_AW_PROMPT_6fdda29ef119f2eb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_9f0aa82a79e6ab0b_EOF' + cat << 'GH_AW_PROMPT_6fdda29ef119f2eb_EOF' {{#runtime-import .github/workflows/shared/community-attribution.md}} {{#runtime-import .github/workflows/shared/observability-otlp.md}} {{#runtime-import .github/workflows/shared/issue-dedup.md}} {{#runtime-import .github/workflows/daily-community-attribution.md}} - GH_AW_PROMPT_9f0aa82a79e6ab0b_EOF + GH_AW_PROMPT_6fdda29ef119f2eb_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPERIMENTS_PROMPT_STYLE: ${{ steps.pick-experiment.outputs.prompt_style }} with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -259,6 +298,7 @@ jobs: uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPERIMENTS_PROMPT_STYLE: ${{ steps.pick-experiment.outputs.prompt_style }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} @@ -285,6 +325,7 @@ jobs: return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, substitutions: { + GH_AW_EXPERIMENTS_PROMPT_STYLE: process.env.GH_AW_EXPERIMENTS_PROMPT_STYLE, GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, @@ -476,9 +517,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_9502a2362f19fb92_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_8fa82c3e0e68cf21_EOF' {"create_issue":{"close_older_issues":true,"expires":168,"group_by_day":true,"labels":["community","automation"],"max":1,"title_prefix":"[community-attribution] "},"create_pull_request":{"draft":true,"expires":24,"labels":["community","automation"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"title_prefix":"[community] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":10240}]},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_9502a2362f19fb92_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_8fa82c3e0e68cf21_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -721,7 +762,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_6160ad4592ea3e1e_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_a286fd94f3c4a2ac_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -768,7 +809,7 @@ jobs: } } } - GH_AW_MCP_CONFIG_6160ad4592ea3e1e_EOF + GH_AW_MCP_CONFIG_a286fd94f3c4a2ac_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1232,6 +1273,12 @@ jobs: mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Download experiment artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: experiment + path: /tmp/gh-aw/experiments/ - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/daily-community-attribution.md b/.github/workflows/daily-community-attribution.md index b813789da82..0daed8857aa 100644 --- a/.github/workflows/daily-community-attribution.md +++ b/.github/workflows/daily-community-attribution.md @@ -53,6 +53,9 @@ safe-outputs: group-by-day: true expires: 7d +experiments: + prompt_style: [concise, verbose] + imports: - shared/community-attribution.md - shared/observability-otlp.md @@ -263,6 +266,13 @@ cat /tmp/gh-aw/repo-memory-default/Community-Contributors.md 2>/dev/null || echo ## Workflow +{{#if (eq experiments.prompt_style "concise")}} +### 1. Attribute Issues + +Read `pre_attributed.json` (Tier 0–2, pre-computed). For each entry in +`tier3_candidates_capped.json` (≤5), apply Tier 3 (one `issue_read` call per +issue). Anything unresolved → Tier 4. +{{else}} ### 1. Attribute All Resolved Community Issues **Tier 0, 1, and 2 attributions are already pre-computed** in @@ -276,7 +286,16 @@ look for indirect linkage via follow-up or split issues). Any candidate still unresolved after Tier 3 becomes a **Tier 4** "needs review" item. Issues in `tier3_candidates.json` beyond the first 5 are deferred to the next run — do not attempt to process them. +{{/if}} + +{{#if (eq experiments.prompt_style "concise")}} +### 2. Update Wiki Page +Merge all attributions into `/tmp/gh-aw/repo-memory-default/Community-Contributors.md`. +Group by author (alphabetical), issues descending. Keep under 9 KB (remove oldest +entries from most-prolific author if needed). Format: +`- [#N](url) Title — YYYY-MM-DD — attribution_type`. Write back with edit tool. +{{else}} ### 2. Update the Community Contributors Wiki Page Read the existing wiki page at @@ -317,7 +336,17 @@ The wiki page format: Write the updated content back to `/tmp/gh-aw/repo-memory-default/Community-Contributors.md` using the edit tool. +{{/if}} +{{#if (eq experiments.prompt_style "concise")}} +### 3. Build Community Section + +Produce a `## 🌍 Community Contributions` `
` block. One bullet per +author (alphabetical), issues descending. Use `#N` refs (no full URLs). Suffixes: +`_(direct issue)_` (T0), none (T1/2), `_(via follow-up #M)_` (T3). Append +`### ⚠️ Attribution Candidates Need Review` section for Tier 4 items. Leave a +blank line after `
`. +{{else}} ### 3. Build the Community Contributions Section Produce a compact section of attributed community contributors for @@ -360,7 +389,14 @@ linked to a specific merged PR. Please verify whether they should be credited: - **@author** for [Issue title](#N) — closed DATE ``` +{{/if}} + +{{#if (eq experiments.prompt_style "concise")}} +### 4. Update README.md +Replace `## 🌍 Community Contributions` in `README.md` with the new content +(or append after `## Contributing` if absent). Use edit tool. +{{else}} ### 4. Update README.md Replace the existing `## 🌍 Community Contributions` section in `README.md` @@ -368,7 +404,15 @@ with the newly generated content, or append it after the `## Contributing` section if it does not yet exist. Use the edit tool to make the change in-place. +{{/if}} + +{{#if (eq experiments.prompt_style "concise")}} +### 5. Open Pull Request +If `README.md` or wiki changed: call `create_pull_request` with title +`[community] Update community contributions in README`. If no changes: +call `noop`. +{{else}} ### 5. Open a Pull Request If `README.md` **or** the wiki page changed, call the `create_pull_request` @@ -398,7 +442,18 @@ and the Community Contributors wiki page. ```json {"noop": {"message": "No action needed: [brief explanation]"}} ``` - +{{/if}} + +{{#if (eq experiments.prompt_style "concise")}} +## Token Budget + +- Read each data file once only +- Process only `tier3_candidates_capped.json` (≤5 issues) +- One `issue_read` per Tier 3 candidate +- Stop after safe-output call +- PR body under 400 words +- No external URLs +{{else}} ## Token Budget Guidelines This workflow uses the Copilot engine — max-turns is not available. Follow these rules to avoid runaway token consumption: @@ -409,7 +464,14 @@ This workflow uses the Copilot engine — max-turns is not available. Follow the - **Stop immediately after the safe-output call** — once `create_pull_request` or `noop` is called, halt without any further tool calls or reasoning - **Keep the PR body under 400 words** — use `
` for any extended attribution summary - **Do not access any external URLs** — use only GitHub MCP `issue_read` for GitHub data; do not call `gh api` or any external HTTP endpoints directly +{{/if}} + +{{#if (eq experiments.prompt_style "concise")}} +### 6. Report Failures +On error: call `create_issue` safe-output tool with a brief title and body. +Do not use GitHub MCP `create_issue` directly. +{{else}} ### 6. Report Failures If you encounter a genuine error that prevents completion (e.g., data fetch failure, unexpected error), report it using the `create_issue` safe-output tool — **never use the GitHub MCP `create_issue` tool directly**. The safe-output tool has built-in deduplication (`group-by-day` and `close-older-issues`) that prevents duplicate failure issues from accumulating. @@ -417,3 +479,4 @@ If you encounter a genuine error that prevents completion (e.g., data fetch fail ```json {"create_issue": {"title": "Brief description of the failure", "body": "### What failed\n\nDescribe the specific step or data source that failed.\n\n### Error details\n\n(Include the error message or unexpected output here)\n\n### Steps to investigate\n\n1. Check the workflow run logs for the full error message\n2. Verify that community_issues.json and pull_requests.json were fetched successfully\n3. Re-run the workflow manually via workflow_dispatch to see if the failure is transient"}} ``` +{{/if}} From 5a3256cd4960c849642da90b9a767f50d11314cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 20:43:50 +0000 Subject: [PATCH 3/6] fix: strengthen concise variants to preserve critical operational instructions - Section 1: add deferred-processing note (issues beyond 5 deferred to next run) - Section 2: add explicit "read the existing wiki page" before merge instruction Agent-Logs-Url: https://github.com/github/gh-aw/sessions/fc6233bc-38b0-43b7-9e11-4a0a345419b3 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/daily-community-attribution.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/daily-community-attribution.md b/.github/workflows/daily-community-attribution.md index 0daed8857aa..64af3577c25 100644 --- a/.github/workflows/daily-community-attribution.md +++ b/.github/workflows/daily-community-attribution.md @@ -269,9 +269,10 @@ cat /tmp/gh-aw/repo-memory-default/Community-Contributors.md 2>/dev/null || echo {{#if (eq experiments.prompt_style "concise")}} ### 1. Attribute Issues -Read `pre_attributed.json` (Tier 0–2, pre-computed). For each entry in -`tier3_candidates_capped.json` (≤5), apply Tier 3 (one `issue_read` call per -issue). Anything unresolved → Tier 4. +Read `pre_attributed.json` (Tier 0–2, pre-computed — do not re-derive). For each +entry in `tier3_candidates_capped.json` (≤5), apply Tier 3 (one `issue_read` call per +issue). Anything unresolved → Tier 4. Issues beyond the first 5 in +`tier3_candidates.json` are deferred to the next run — do not process them. {{else}} ### 1. Attribute All Resolved Community Issues @@ -291,9 +292,10 @@ are deferred to the next run — do not attempt to process them. {{#if (eq experiments.prompt_style "concise")}} ### 2. Update Wiki Page -Merge all attributions into `/tmp/gh-aw/repo-memory-default/Community-Contributors.md`. -Group by author (alphabetical), issues descending. Keep under 9 KB (remove oldest -entries from most-prolific author if needed). Format: +Read the existing wiki at `/tmp/gh-aw/repo-memory-default/Community-Contributors.md` +(empty/missing on first run). Merge all confirmed attributions without duplicating +entries. Group by author (alphabetical), issues descending. Keep under 9 KB (remove +oldest entries from most-prolific author if needed). Format: `- [#N](url) Title — YYYY-MM-DD — attribution_type`. Write back with edit tool. {{else}} ### 2. Update the Community Contributors Wiki Page From 3aac568d2d9ed4f4a69b40399b6a4b3e48428e8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 21:01:50 +0000 Subject: [PATCH 4/6] feat: add {{else}} and (eq VALUE LITERAL) support to template engine - is_truthy.cjs: handle (eq VALUE "LITERAL") equality helper expressions so {{#if (eq concise "concise")}} evaluates correctly after experiment placeholder substitution - interpolate_prompt.cjs: split {{#if}}...{{else}}...{{/if}} blocks on the {{else}} separator and keep the appropriate branch based on truthiness - interpolate_prompt.cjs step 2.5: substitute experiments.NAME references inside {{#if ...}} conditions (in addition to __PLACEHOLDER__ forms) so (eq experiments.prompt_style "value") conditions resolve to actual values - is_truthy.test.cjs: add tests for (eq ...) helper - interpolate_prompt.test.cjs: add tests for {{else}} and (eq ...) rendering Agent-Logs-Url: https://github.com/github/gh-aw/sessions/ae04afdf-c413-4a21-bbbe-1632b9f369f0 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/interpolate_prompt.cjs | 32 +++++++++++++++++--- actions/setup/js/interpolate_prompt.test.cjs | 21 +++++++++++++ actions/setup/js/is_truthy.cjs | 19 ++++++++++-- actions/setup/js/is_truthy.test.cjs | 23 ++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/actions/setup/js/interpolate_prompt.cjs b/actions/setup/js/interpolate_prompt.cjs index 6238b2d035e..13f661a9f28 100644 --- a/actions/setup/js/interpolate_prompt.cjs +++ b/actions/setup/js/interpolate_prompt.cjs @@ -88,14 +88,24 @@ function renderMarkdownTemplate(markdown) { core.info(`[renderMarkdownTemplate] Block ${blockCount}: condition="${condTrimmed}" -> ${truthyResult ? "KEEP" : "REMOVE"}`); core.info(`[renderMarkdownTemplate] Body preview: "${bodyPreview}${body.length > 60 ? "..." : ""}"`); + // Split on {{else}} if present to support two-branch conditionals. + // e.g. {{#if (eq concise "concise")}} ... {{else}} ... {{/if}} + const elseParts = body.split(/[ \t]*\{\{else\}\}[ \t]*\n?/); + const trueBranch = elseParts[0]; + const falseBranch = elseParts.length > 1 ? elseParts.slice(1).join("{{else}}") : null; + if (truthyResult) { - // Keep body with leading newline if there was one before the opening tag + // Keep the true branch (before {{else}}, or full body if no {{else}}) keptBlocks++; - core.info(`[renderMarkdownTemplate] Action: Keeping body with leading newline=${!!leadNL}`); - return leadNL + body; + core.info(`[renderMarkdownTemplate] Action: Keeping ${falseBranch !== null ? "true branch" : "body"} with leading newline=${!!leadNL}`); + return leadNL + trueBranch; } else { - // Remove entire block completely - the line containing the template is removed + // Remove the block, or keep the false branch when {{else}} is present removedBlocks++; + if (falseBranch !== null) { + core.info(`[renderMarkdownTemplate] Action: Keeping false branch ({{else}} branch)`); + return leadNL + falseBranch; + } core.info(`[renderMarkdownTemplate] Action: Removing entire block`); return ""; } @@ -244,6 +254,10 @@ async function main() { // otherwise the placeholder string is truthy and the block is always kept. // The activation job exposes GH_AW_EXPERIMENTS_* env vars (from the pick-experiment // step output via the step's env: block), so we can substitute them here. + // + // Additionally, {{#if (eq experiments.name "value")}} conditions use the dot-notation + // form directly in the condition expression. We substitute experiments.NAME → actual + // value inside {{#if ...}} condition tags so that isTruthy can evaluate (eq ...) helpers. core.info("\n========================================"); core.info("[main] STEP 2.5: Experiment Placeholder Substitution"); core.info("========================================"); @@ -256,6 +270,16 @@ async function main() { experimentSubCount++; core.info(` Substituted ${placeholder} → "${value || ""}"`); } + // Also substitute experiments.name references inside {{#if ...}} conditions. + // This enables (eq experiments.name "value") comparisons to resolve correctly. + const experimentName = key.substring("GH_AW_EXPERIMENTS_".length).toLowerCase(); + const exprForm = `experiments.${experimentName}`; + const conditionPattern = new RegExp(`(\\{\\{#if[^}]*?)${exprForm.replace(".", "\\.")}`, "gi"); + if (conditionPattern.test(content)) { + conditionPattern.lastIndex = 0; + content = content.replace(conditionPattern, `$1${value || ""}`); + core.info(` Substituted ${exprForm} in conditions → "${value || ""}"`); + } } } if (experimentSubCount > 0) { diff --git a/actions/setup/js/interpolate_prompt.test.cjs b/actions/setup/js/interpolate_prompt.test.cjs index fa9cb02ea1f..b45080a8672 100644 --- a/actions/setup/js/interpolate_prompt.test.cjs +++ b/actions/setup/js/interpolate_prompt.test.cjs @@ -89,6 +89,27 @@ describe("interpolate_prompt", () => { it("should collapse multiple false blocks without excessive empty lines", () => { const output = renderMarkdownTemplate("Start\n\n{{#if false}}\nBlock 1\n{{/if}}\n\n{{#if false}}\nBlock 2\n{{/if}}\n\n{{#if false}}\nBlock 3\n{{/if}}\n\nEnd"); (expect(output).not.toMatch(/\n{3,}/), expect(output).toContain("Start"), expect(output).toContain("End")); + }), + it("should keep true branch of {{else}} block when condition is truthy", () => { + const output = renderMarkdownTemplate("{{#if true}}\nTrue branch\n{{else}}\nFalse branch\n{{/if}}"); + expect(output).toContain("True branch"); + expect(output).not.toContain("False branch"); + expect(output).not.toContain("{{else}}"); + }), + it("should keep false branch of {{else}} block when condition is falsy", () => { + const output = renderMarkdownTemplate("{{#if false}}\nTrue branch\n{{else}}\nFalse branch\n{{/if}}"); + expect(output).toContain("False branch"); + expect(output).not.toContain("True branch"); + expect(output).not.toContain("{{else}}"); + }), + it("should handle {{else}} with (eq VALUE LITERAL) condition matching", () => { + // Simulates what happens after experiment substitution: (eq concise "concise") + const conciseOutput = renderMarkdownTemplate('{{#if (eq concise "concise")}}\nConcise content\n{{else}}\nVerbose content\n{{/if}}'); + expect(conciseOutput).toContain("Concise content"); + expect(conciseOutput).not.toContain("Verbose content"); + const verboseOutput = renderMarkdownTemplate('{{#if (eq verbose "concise")}}\nConcise content\n{{else}}\nVerbose content\n{{/if}}'); + expect(verboseOutput).toContain("Verbose content"); + expect(verboseOutput).not.toContain("Concise content"); })); }), describe("combined interpolation and template rendering", () => { diff --git a/actions/setup/js/is_truthy.cjs b/actions/setup/js/is_truthy.cjs index a0464faa605..0900ed649ba 100644 --- a/actions/setup/js/is_truthy.cjs +++ b/actions/setup/js/is_truthy.cjs @@ -1,11 +1,26 @@ // @ts-check /** - * Determines if a value is truthy according to template logic + * Determines if a value is truthy according to template logic. + * + * Supports: + * - Simple falsy string check: "", "false", "no", "0", "null", "undefined" + * - Equality helper: (eq VALUE "LITERAL") — returns true when VALUE === LITERAL + * * @param {string} expr - The expression to evaluate * @returns {boolean} - Whether the expression is truthy */ function isTruthy(expr) { - const v = expr.trim().toLowerCase(); + const trimmed = expr.trim(); + + // Handle (eq VALUE "LITERAL") helper expression. + // Used by experiment conditionals: {{#if (eq concise "concise")}} + // after the experiment placeholder has been substituted into the condition. + const eqMatch = trimmed.match(/^\(eq\s+(.+?)\s+"(.+?)"\)$/i); + if (eqMatch) { + return eqMatch[1].trim() === eqMatch[2].trim(); + } + + const v = trimmed.toLowerCase(); return !(v === "" || v === "false" || v === "no" || v === "0" || v === "null" || v === "undefined"); } diff --git a/actions/setup/js/is_truthy.test.cjs b/actions/setup/js/is_truthy.test.cjs index eb7ea89aa26..7ad3bbd6a84 100644 --- a/actions/setup/js/is_truthy.test.cjs +++ b/actions/setup/js/is_truthy.test.cjs @@ -63,5 +63,28 @@ describe("is_truthy.cjs", () => { expect(isTruthy("NuLl")).toBe(false); expect(isTruthy("UnDeFiNeD")).toBe(false); }); + + describe("(eq VALUE LITERAL) helper", () => { + it("should return true when value matches literal", () => { + expect(isTruthy('(eq concise "concise")')).toBe(true); + expect(isTruthy('(eq verbose "verbose")')).toBe(true); + expect(isTruthy('(eq yes "yes")')).toBe(true); + }); + + it("should return false when value does not match literal", () => { + expect(isTruthy('(eq verbose "concise")')).toBe(false); + expect(isTruthy('(eq concise "verbose")')).toBe(false); + expect(isTruthy('(eq no "yes")')).toBe(false); + }); + + it("should be case-insensitive for the (eq ...) wrapper but case-sensitive for values", () => { + expect(isTruthy('(EQ concise "concise")')).toBe(true); + expect(isTruthy('(Eq concise "concise")')).toBe(true); + }); + + it("should handle values with spaces via trim", () => { + expect(isTruthy('(eq concise "concise")')).toBe(true); + }); + }); }); }); From 6502dc5832bb969e8b7202b31079093b87e96260 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 21:15:35 +0000 Subject: [PATCH 5/6] fix: use {{#else}}/{{#endif}} syntax and v == b style per reviewer feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - daily-community-attribution.md: {{else}} → {{#else}}, {{/if}} → {{#endif}}, (eq experiments.prompt_style "concise") → experiments.prompt_style == "concise" - is_truthy.cjs: replace (eq v b) helper with GitHub Actions script style v == "b" / v === "b" / v != "b" / v !== "b" equality expressions - interpolate_prompt.cjs: {{else}} split → {{#else}}; closing tag regex now matches {{#endif}} (primary) or {{/if}} (alternate) in both passes; update comments - is_truthy.test.cjs: replace (eq ...) tests with v == b style tests - interpolate_prompt.test.cjs: update new tests to use {{#else}}/{{#endif}}/v==b; add test verifying {{/if}} alternate closing tag still works Agent-Logs-Url: https://github.com/github/gh-aw/sessions/be990686-bfaf-4163-8e65-1b4c281f0a98 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../workflows/daily-community-attribution.md | 42 +++++++++---------- actions/setup/js/interpolate_prompt.cjs | 24 ++++++----- actions/setup/js/interpolate_prompt.test.cjs | 24 ++++++----- actions/setup/js/is_truthy.cjs | 17 +++++--- actions/setup/js/is_truthy.test.cjs | 39 ++++++++++------- 5 files changed, 84 insertions(+), 62 deletions(-) diff --git a/.github/workflows/daily-community-attribution.md b/.github/workflows/daily-community-attribution.md index 64af3577c25..7acbe4e27ca 100644 --- a/.github/workflows/daily-community-attribution.md +++ b/.github/workflows/daily-community-attribution.md @@ -266,14 +266,14 @@ cat /tmp/gh-aw/repo-memory-default/Community-Contributors.md 2>/dev/null || echo ## Workflow -{{#if (eq experiments.prompt_style "concise")}} +{{#if experiments.prompt_style == "concise"}} ### 1. Attribute Issues Read `pre_attributed.json` (Tier 0–2, pre-computed — do not re-derive). For each entry in `tier3_candidates_capped.json` (≤5), apply Tier 3 (one `issue_read` call per issue). Anything unresolved → Tier 4. Issues beyond the first 5 in `tier3_candidates.json` are deferred to the next run — do not process them. -{{else}} +{{#else}} ### 1. Attribute All Resolved Community Issues **Tier 0, 1, and 2 attributions are already pre-computed** in @@ -287,9 +287,9 @@ look for indirect linkage via follow-up or split issues). Any candidate still unresolved after Tier 3 becomes a **Tier 4** "needs review" item. Issues in `tier3_candidates.json` beyond the first 5 are deferred to the next run — do not attempt to process them. -{{/if}} +{{#endif}} -{{#if (eq experiments.prompt_style "concise")}} +{{#if experiments.prompt_style == "concise"}} ### 2. Update Wiki Page Read the existing wiki at `/tmp/gh-aw/repo-memory-default/Community-Contributors.md` @@ -297,7 +297,7 @@ Read the existing wiki at `/tmp/gh-aw/repo-memory-default/Community-Contributors entries. Group by author (alphabetical), issues descending. Keep under 9 KB (remove oldest entries from most-prolific author if needed). Format: `- [#N](url) Title — YYYY-MM-DD — attribution_type`. Write back with edit tool. -{{else}} +{{#else}} ### 2. Update the Community Contributors Wiki Page Read the existing wiki page at @@ -338,9 +338,9 @@ The wiki page format: Write the updated content back to `/tmp/gh-aw/repo-memory-default/Community-Contributors.md` using the edit tool. -{{/if}} +{{#endif}} -{{#if (eq experiments.prompt_style "concise")}} +{{#if experiments.prompt_style == "concise"}} ### 3. Build Community Section Produce a `## 🌍 Community Contributions` `
` block. One bullet per @@ -348,7 +348,7 @@ author (alphabetical), issues descending. Use `#N` refs (no full URLs). Suffixes `_(direct issue)_` (T0), none (T1/2), `_(via follow-up #M)_` (T3). Append `### ⚠️ Attribution Candidates Need Review` section for Tier 4 items. Leave a blank line after `
`. -{{else}} +{{#else}} ### 3. Build the Community Contributions Section Produce a compact section of attributed community contributors for @@ -391,14 +391,14 @@ linked to a specific merged PR. Please verify whether they should be credited: - **@author** for [Issue title](#N) — closed DATE ``` -{{/if}} +{{#endif}} -{{#if (eq experiments.prompt_style "concise")}} +{{#if experiments.prompt_style == "concise"}} ### 4. Update README.md Replace `## 🌍 Community Contributions` in `README.md` with the new content (or append after `## Contributing` if absent). Use edit tool. -{{else}} +{{#else}} ### 4. Update README.md Replace the existing `## 🌍 Community Contributions` section in `README.md` @@ -406,15 +406,15 @@ with the newly generated content, or append it after the `## Contributing` section if it does not yet exist. Use the edit tool to make the change in-place. -{{/if}} +{{#endif}} -{{#if (eq experiments.prompt_style "concise")}} +{{#if experiments.prompt_style == "concise"}} ### 5. Open Pull Request If `README.md` or wiki changed: call `create_pull_request` with title `[community] Update community contributions in README`. If no changes: call `noop`. -{{else}} +{{#else}} ### 5. Open a Pull Request If `README.md` **or** the wiki page changed, call the `create_pull_request` @@ -444,9 +444,9 @@ and the Community Contributors wiki page. ```json {"noop": {"message": "No action needed: [brief explanation]"}} ``` -{{/if}} +{{#endif}} -{{#if (eq experiments.prompt_style "concise")}} +{{#if experiments.prompt_style == "concise"}} ## Token Budget - Read each data file once only @@ -455,7 +455,7 @@ and the Community Contributors wiki page. - Stop after safe-output call - PR body under 400 words - No external URLs -{{else}} +{{#else}} ## Token Budget Guidelines This workflow uses the Copilot engine — max-turns is not available. Follow these rules to avoid runaway token consumption: @@ -466,14 +466,14 @@ This workflow uses the Copilot engine — max-turns is not available. Follow the - **Stop immediately after the safe-output call** — once `create_pull_request` or `noop` is called, halt without any further tool calls or reasoning - **Keep the PR body under 400 words** — use `
` for any extended attribution summary - **Do not access any external URLs** — use only GitHub MCP `issue_read` for GitHub data; do not call `gh api` or any external HTTP endpoints directly -{{/if}} +{{#endif}} -{{#if (eq experiments.prompt_style "concise")}} +{{#if experiments.prompt_style == "concise"}} ### 6. Report Failures On error: call `create_issue` safe-output tool with a brief title and body. Do not use GitHub MCP `create_issue` directly. -{{else}} +{{#else}} ### 6. Report Failures If you encounter a genuine error that prevents completion (e.g., data fetch failure, unexpected error), report it using the `create_issue` safe-output tool — **never use the GitHub MCP `create_issue` tool directly**. The safe-output tool has built-in deduplication (`group-by-day` and `close-older-issues`) that prevents duplicate failure issues from accumulating. @@ -481,4 +481,4 @@ If you encounter a genuine error that prevents completion (e.g., data fetch fail ```json {"create_issue": {"title": "Brief description of the failure", "body": "### What failed\n\nDescribe the specific step or data source that failed.\n\n### Error details\n\n(Include the error message or unexpected output here)\n\n### Steps to investigate\n\n1. Check the workflow run logs for the full error message\n2. Verify that community_issues.json and pull_requests.json were fetched successfully\n3. Re-run the workflow manually via workflow_dispatch to see if the failure is transient"}} ``` -{{/if}} +{{#endif}} diff --git a/actions/setup/js/interpolate_prompt.cjs b/actions/setup/js/interpolate_prompt.cjs index 13f661a9f28..e419927e6cf 100644 --- a/actions/setup/js/interpolate_prompt.cjs +++ b/actions/setup/js/interpolate_prompt.cjs @@ -68,8 +68,8 @@ function renderMarkdownTemplate(markdown) { } // Count conditionals before processing - const blockConditionals = (_stripped.match(/(\n?)([ \t]*{{#if\s+([^}]*)}}[ \t]*\n)([\s\S]*?)([ \t]*{{\/if}}[ \t]*)(\n?)/g) || []).length; - const inlineConditionals = (_stripped.match(/{{#if\s+([^}]*)}}([\s\S]*?){{\/if}}/g) || []).length - blockConditionals; + const blockConditionals = (_stripped.match(/(\n?)([ \t]*{{#if\s+([^}]*)}}[ \t]*\n)([\s\S]*?)([ \t]*(?:{{#endif}}|{{\/if}})[ \t]*)(\n?)/g) || []).length; + const inlineConditionals = (_stripped.match(/{{#if\s+([^}]*)}}([\s\S]*?)(?:{{#endif}}|{{\/if}})/g) || []).length - blockConditionals; core.info(`[renderMarkdownTemplate] Found ${blockConditionals} block conditional(s) and ${inlineConditionals} inline conditional(s)`); @@ -79,7 +79,8 @@ function renderMarkdownTemplate(markdown) { // First pass: Handle blocks where tags are on their own lines // Captures: (leading newline)(opening tag line)(condition)(body)(closing tag line)(trailing newline) - let result = _stripped.replace(/(\n?)([ \t]*{{#if\s+([^}]*)}}[ \t]*\n)([\s\S]*?)([ \t]*{{\/if}}[ \t]*)(\n?)/g, (match, leadNL, openLine, cond, body, closeLine, trailNL) => { + // Closing tag: {{#endif}} (primary) or {{/if}} (alternate) + let result = _stripped.replace(/(\n?)([ \t]*{{#if\s+([^}]*)}}[ \t]*\n)([\s\S]*?)([ \t]*(?:{{#endif}}|{{\/if}})[ \t]*)(\n?)/g, (match, leadNL, openLine, cond, body, closeLine, trailNL) => { blockCount++; const condTrimmed = cond.trim(); const truthyResult = isTruthy(cond); @@ -88,22 +89,22 @@ function renderMarkdownTemplate(markdown) { core.info(`[renderMarkdownTemplate] Block ${blockCount}: condition="${condTrimmed}" -> ${truthyResult ? "KEEP" : "REMOVE"}`); core.info(`[renderMarkdownTemplate] Body preview: "${bodyPreview}${body.length > 60 ? "..." : ""}"`); - // Split on {{else}} if present to support two-branch conditionals. - // e.g. {{#if (eq concise "concise")}} ... {{else}} ... {{/if}} - const elseParts = body.split(/[ \t]*\{\{else\}\}[ \t]*\n?/); + // Split on {{#else}} if present to support two-branch conditionals. + // e.g. {{#if experiments.prompt_style == "concise"}} ... {{#else}} ... {{#endif}} + const elseParts = body.split(/[ \t]*\{\{#else\}\}[ \t]*\n?/); const trueBranch = elseParts[0]; - const falseBranch = elseParts.length > 1 ? elseParts.slice(1).join("{{else}}") : null; + const falseBranch = elseParts.length > 1 ? elseParts.slice(1).join("{{#else}}") : null; if (truthyResult) { - // Keep the true branch (before {{else}}, or full body if no {{else}}) + // Keep the true branch (before {{#else}}, or full body if no {{#else}}) keptBlocks++; core.info(`[renderMarkdownTemplate] Action: Keeping ${falseBranch !== null ? "true branch" : "body"} with leading newline=${!!leadNL}`); return leadNL + trueBranch; } else { - // Remove the block, or keep the false branch when {{else}} is present + // Remove the block, or keep the false branch when {{#else}} is present removedBlocks++; if (falseBranch !== null) { - core.info(`[renderMarkdownTemplate] Action: Keeping false branch ({{else}} branch)`); + core.info(`[renderMarkdownTemplate] Action: Keeping false branch ({{#else}} branch)`); return leadNL + falseBranch; } core.info(`[renderMarkdownTemplate] Action: Removing entire block`); @@ -118,7 +119,8 @@ function renderMarkdownTemplate(markdown) { let removedInline = 0; // Second pass: Handle inline conditionals (tags not on their own lines) - result = result.replace(/{{#if\s+([^}]*)}}([\s\S]*?){{\/if}}/g, (_, cond, body) => { + // Closing tag: {{#endif}} (primary) or {{/if}} (alternate) + result = result.replace(/{{#if\s+([^}]*)}}([\s\S]*?)(?:{{#endif}}|{{\/if}})/g, (_, cond, body) => { inlineCount++; const condTrimmed = cond.trim(); const truthyResult = isTruthy(cond); diff --git a/actions/setup/js/interpolate_prompt.test.cjs b/actions/setup/js/interpolate_prompt.test.cjs index b45080a8672..13c6c418e2a 100644 --- a/actions/setup/js/interpolate_prompt.test.cjs +++ b/actions/setup/js/interpolate_prompt.test.cjs @@ -90,26 +90,30 @@ describe("interpolate_prompt", () => { const output = renderMarkdownTemplate("Start\n\n{{#if false}}\nBlock 1\n{{/if}}\n\n{{#if false}}\nBlock 2\n{{/if}}\n\n{{#if false}}\nBlock 3\n{{/if}}\n\nEnd"); (expect(output).not.toMatch(/\n{3,}/), expect(output).toContain("Start"), expect(output).toContain("End")); }), - it("should keep true branch of {{else}} block when condition is truthy", () => { - const output = renderMarkdownTemplate("{{#if true}}\nTrue branch\n{{else}}\nFalse branch\n{{/if}}"); + it("should keep true branch of {{#else}} block when condition is truthy", () => { + const output = renderMarkdownTemplate("{{#if true}}\nTrue branch\n{{#else}}\nFalse branch\n{{#endif}}"); expect(output).toContain("True branch"); expect(output).not.toContain("False branch"); - expect(output).not.toContain("{{else}}"); + expect(output).not.toContain("{{#else}}"); }), - it("should keep false branch of {{else}} block when condition is falsy", () => { - const output = renderMarkdownTemplate("{{#if false}}\nTrue branch\n{{else}}\nFalse branch\n{{/if}}"); + it("should keep false branch of {{#else}} block when condition is falsy", () => { + const output = renderMarkdownTemplate("{{#if false}}\nTrue branch\n{{#else}}\nFalse branch\n{{#endif}}"); expect(output).toContain("False branch"); expect(output).not.toContain("True branch"); - expect(output).not.toContain("{{else}}"); + expect(output).not.toContain("{{#else}}"); }), - it("should handle {{else}} with (eq VALUE LITERAL) condition matching", () => { - // Simulates what happens after experiment substitution: (eq concise "concise") - const conciseOutput = renderMarkdownTemplate('{{#if (eq concise "concise")}}\nConcise content\n{{else}}\nVerbose content\n{{/if}}'); + it("should handle {{#else}} with GitHub Actions style equality condition matching", () => { + // Simulates what happens after experiment substitution: concise == "concise" + const conciseOutput = renderMarkdownTemplate('{{#if concise == "concise"}}\nConcise content\n{{#else}}\nVerbose content\n{{#endif}}'); expect(conciseOutput).toContain("Concise content"); expect(conciseOutput).not.toContain("Verbose content"); - const verboseOutput = renderMarkdownTemplate('{{#if (eq verbose "concise")}}\nConcise content\n{{else}}\nVerbose content\n{{/if}}'); + const verboseOutput = renderMarkdownTemplate('{{#if verbose == "concise"}}\nConcise content\n{{#else}}\nVerbose content\n{{#endif}}'); expect(verboseOutput).toContain("Verbose content"); expect(verboseOutput).not.toContain("Concise content"); + }), + it("should support {{/if}} as alternate closing tag", () => { + const output = renderMarkdownTemplate("{{#if true}}\nKeep\n{{/if}}"); + expect(output).toContain("Keep"); })); }), describe("combined interpolation and template rendering", () => { diff --git a/actions/setup/js/is_truthy.cjs b/actions/setup/js/is_truthy.cjs index 0900ed649ba..54f704b5d9b 100644 --- a/actions/setup/js/is_truthy.cjs +++ b/actions/setup/js/is_truthy.cjs @@ -4,7 +4,8 @@ * * Supports: * - Simple falsy string check: "", "false", "no", "0", "null", "undefined" - * - Equality helper: (eq VALUE "LITERAL") — returns true when VALUE === LITERAL + * - GitHub Actions script style equality: lhs == "rhs" or lhs === "rhs" + * After experiment substitution the condition looks like: concise == "concise" * * @param {string} expr - The expression to evaluate * @returns {boolean} - Whether the expression is truthy @@ -12,12 +13,16 @@ function isTruthy(expr) { const trimmed = expr.trim(); - // Handle (eq VALUE "LITERAL") helper expression. - // Used by experiment conditionals: {{#if (eq concise "concise")}} - // after the experiment placeholder has been substituted into the condition. - const eqMatch = trimmed.match(/^\(eq\s+(.+?)\s+"(.+?)"\)$/i); + // Handle GitHub Actions script style equality expressions: lhs == "rhs" or lhs === "rhs" + // Used by experiment conditionals after the experiment value has been substituted: + // {{#if experiments.prompt_style == "concise"}} becomes {{#if concise == "concise"}} + const eqMatch = trimmed.match(/^(.+?)\s*===?\s*"([^"]*)"\s*$/); if (eqMatch) { - return eqMatch[1].trim() === eqMatch[2].trim(); + return eqMatch[1].trim() === eqMatch[2]; + } + const neqMatch = trimmed.match(/^(.+?)\s*!==?\s*"([^"]*)"\s*$/); + if (neqMatch) { + return neqMatch[1].trim() !== neqMatch[2]; } const v = trimmed.toLowerCase(); diff --git a/actions/setup/js/is_truthy.test.cjs b/actions/setup/js/is_truthy.test.cjs index 7ad3bbd6a84..66cd8fa3e0f 100644 --- a/actions/setup/js/is_truthy.test.cjs +++ b/actions/setup/js/is_truthy.test.cjs @@ -64,26 +64,37 @@ describe("is_truthy.cjs", () => { expect(isTruthy("UnDeFiNeD")).toBe(false); }); - describe("(eq VALUE LITERAL) helper", () => { - it("should return true when value matches literal", () => { - expect(isTruthy('(eq concise "concise")')).toBe(true); - expect(isTruthy('(eq verbose "verbose")')).toBe(true); - expect(isTruthy('(eq yes "yes")')).toBe(true); + describe("GitHub Actions script style equality (v == b)", () => { + it("should return true when value matches literal with ==", () => { + expect(isTruthy('concise == "concise"')).toBe(true); + expect(isTruthy('verbose == "verbose"')).toBe(true); + expect(isTruthy('yes == "yes"')).toBe(true); }); - it("should return false when value does not match literal", () => { - expect(isTruthy('(eq verbose "concise")')).toBe(false); - expect(isTruthy('(eq concise "verbose")')).toBe(false); - expect(isTruthy('(eq no "yes")')).toBe(false); + it("should return false when value does not match literal with ==", () => { + expect(isTruthy('verbose == "concise"')).toBe(false); + expect(isTruthy('concise == "verbose"')).toBe(false); + expect(isTruthy('no == "yes"')).toBe(false); }); - it("should be case-insensitive for the (eq ...) wrapper but case-sensitive for values", () => { - expect(isTruthy('(EQ concise "concise")')).toBe(true); - expect(isTruthy('(Eq concise "concise")')).toBe(true); + it("should return true when value matches literal with ===", () => { + expect(isTruthy('concise === "concise"')).toBe(true); }); - it("should handle values with spaces via trim", () => { - expect(isTruthy('(eq concise "concise")')).toBe(true); + it("should return false when value does not match literal with ===", () => { + expect(isTruthy('verbose === "concise"')).toBe(false); + }); + + it("should return true when value does not match literal with !=", () => { + expect(isTruthy('verbose != "concise"')).toBe(true); + }); + + it("should return false when value matches literal with !=", () => { + expect(isTruthy('concise != "concise"')).toBe(false); + }); + + it("should handle whitespace around operators", () => { + expect(isTruthy(' concise == "concise" ')).toBe(true); }); }); }); From 65a6739872aa0b8dc0804d35599dfffc719741f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 21:39:12 +0000 Subject: [PATCH 6/6] fix: address reviewer feedback on is_truthy, comments, and workflow wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - is_truthy.cjs: fix empty-LHS edge case — change (.+?) to (.*?) so a condition like ' == "concise"' (unset experiment var) correctly returns false instead of falling through to the truthy string check - is_truthy.test.cjs: rename describe block from 'v == b' to 'v == "b"' to accurately reflect that the RHS must be quoted; add empty-LHS test cases - interpolate_prompt.cjs: update Step 2.5 comment to remove stale (eq ...) references and describe the actual lhs == "rhs" syntax used after substitution - daily-community-attribution.md: reword "No external URLs" to "Do not access external URLs; use only GitHub MCP issue_read for GitHub data" for clarity Agent-Logs-Url: https://github.com/github/gh-aw/sessions/704460b1-ad87-4898-846d-0a5207729619 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/daily-community-attribution.md | 2 +- actions/setup/js/interpolate_prompt.cjs | 9 +++++---- actions/setup/js/is_truthy.cjs | 7 +++++-- actions/setup/js/is_truthy.test.cjs | 9 ++++++++- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/workflows/daily-community-attribution.md b/.github/workflows/daily-community-attribution.md index 7acbe4e27ca..52c6cde9194 100644 --- a/.github/workflows/daily-community-attribution.md +++ b/.github/workflows/daily-community-attribution.md @@ -454,7 +454,7 @@ and the Community Contributors wiki page. - One `issue_read` per Tier 3 candidate - Stop after safe-output call - PR body under 400 words -- No external URLs +- Do not access external URLs; use only GitHub MCP `issue_read` for GitHub data {{#else}} ## Token Budget Guidelines diff --git a/actions/setup/js/interpolate_prompt.cjs b/actions/setup/js/interpolate_prompt.cjs index e419927e6cf..95a770730b4 100644 --- a/actions/setup/js/interpolate_prompt.cjs +++ b/actions/setup/js/interpolate_prompt.cjs @@ -256,10 +256,10 @@ async function main() { // otherwise the placeholder string is truthy and the block is always kept. // The activation job exposes GH_AW_EXPERIMENTS_* env vars (from the pick-experiment // step output via the step's env: block), so we can substitute them here. - // - // Additionally, {{#if (eq experiments.name "value")}} conditions use the dot-notation + // Additionally, {{#if experiments.name == "value"}} conditions use the dot-notation // form directly in the condition expression. We substitute experiments.NAME → actual - // value inside {{#if ...}} condition tags so that isTruthy can evaluate (eq ...) helpers. + // value inside {{#if ...}} condition tags so that isTruthy can evaluate the resulting + // GitHub Actions script style expression (e.g. concise == "concise"). core.info("\n========================================"); core.info("[main] STEP 2.5: Experiment Placeholder Substitution"); core.info("========================================"); @@ -273,7 +273,8 @@ async function main() { core.info(` Substituted ${placeholder} → "${value || ""}"`); } // Also substitute experiments.name references inside {{#if ...}} conditions. - // This enables (eq experiments.name "value") comparisons to resolve correctly. + // This enables GitHub Actions script style comparisons (e.g. prompt_style == "concise") + // to resolve correctly — after substitution the condition becomes: concise == "concise". const experimentName = key.substring("GH_AW_EXPERIMENTS_".length).toLowerCase(); const exprForm = `experiments.${experimentName}`; const conditionPattern = new RegExp(`(\\{\\{#if[^}]*?)${exprForm.replace(".", "\\.")}`, "gi"); diff --git a/actions/setup/js/is_truthy.cjs b/actions/setup/js/is_truthy.cjs index 54f704b5d9b..42b6e9354f7 100644 --- a/actions/setup/js/is_truthy.cjs +++ b/actions/setup/js/is_truthy.cjs @@ -16,11 +16,14 @@ function isTruthy(expr) { // Handle GitHub Actions script style equality expressions: lhs == "rhs" or lhs === "rhs" // Used by experiment conditionals after the experiment value has been substituted: // {{#if experiments.prompt_style == "concise"}} becomes {{#if concise == "concise"}} - const eqMatch = trimmed.match(/^(.+?)\s*===?\s*"([^"]*)"\s*$/); + // Note: (.*?) allows an empty LHS — if the experiment variable was not set the substituted + // condition looks like ' == "concise"', which correctly returns false here rather than + // falling through to the generic truthy check and incorrectly returning true. + const eqMatch = trimmed.match(/^(.*?)\s*===?\s*"([^"]*)"\s*$/); if (eqMatch) { return eqMatch[1].trim() === eqMatch[2]; } - const neqMatch = trimmed.match(/^(.+?)\s*!==?\s*"([^"]*)"\s*$/); + const neqMatch = trimmed.match(/^(.*?)\s*!==?\s*"([^"]*)"\s*$/); if (neqMatch) { return neqMatch[1].trim() !== neqMatch[2]; } diff --git a/actions/setup/js/is_truthy.test.cjs b/actions/setup/js/is_truthy.test.cjs index 66cd8fa3e0f..e052d0d0981 100644 --- a/actions/setup/js/is_truthy.test.cjs +++ b/actions/setup/js/is_truthy.test.cjs @@ -64,7 +64,7 @@ describe("is_truthy.cjs", () => { expect(isTruthy("UnDeFiNeD")).toBe(false); }); - describe("GitHub Actions script style equality (v == b)", () => { + describe('GitHub Actions script style equality (v == "b")', () => { it("should return true when value matches literal with ==", () => { expect(isTruthy('concise == "concise"')).toBe(true); expect(isTruthy('verbose == "verbose"')).toBe(true); @@ -96,6 +96,13 @@ describe("is_truthy.cjs", () => { it("should handle whitespace around operators", () => { expect(isTruthy(' concise == "concise" ')).toBe(true); }); + + it("should return false for empty LHS (unset experiment variable)", () => { + // If the experiment env var was not set, substitution leaves an empty LHS: + // {{#if == "concise"}} — should be false, not truthy fallback + expect(isTruthy(' == "concise"')).toBe(false); + expect(isTruthy('== "concise"')).toBe(false); + }); }); }); });