diff --git a/.github/workflows/daily-fact.lock.yml b/.github/workflows/daily-fact.lock.yml
index 5724aefd8fd..696981bde35 100644
--- a/.github/workflows/daily-fact.lock.yml
+++ b/.github/workflows/daily-fact.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"36d37a405b99aa890e631983125c296a231d492607435c20e8632d45621f8ecd","strict":true,"agent_id":"codex","agent_model":"gpt-5.4-mini"}
+# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"4d7cf7739698722a561870045505a39cb9b1f80ad113ae5ca1df484b1bb3ccbd","strict":true,"agent_id":"codex","agent_model":"gpt-5.4-mini"}
# gh-aw-manifest: {"version":1,"secrets":["CODEX_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN","OPENAI_API_KEY"],"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":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.43"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"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"}]}
# ___ _ _
# / _ \ | | (_)
@@ -229,7 +229,7 @@ jobs:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl
GH_AW_EXPERIMENTS_REASONING_DEPTH: ${{ steps.pick-experiment.outputs.reasoning_depth }}
- GH_AW_EXPR_DD64DF46: ${{ steps.pick-experiment.outputs.reasoning_depth == "multi_candidate" }}
+ GH_AW_EXPR_CBA23170: ${{ steps.pick-experiment.outputs.reasoning_depth == 'multi_candidate' }}
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 }}
@@ -242,21 +242,21 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_6f6003e03d2c7d0e_EOF'
+ cat << 'GH_AW_PROMPT_10c047efb98cf84f_EOF'
- GH_AW_PROMPT_6f6003e03d2c7d0e_EOF
+ GH_AW_PROMPT_10c047efb98cf84f_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/cache_memory_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
- cat << 'GH_AW_PROMPT_6f6003e03d2c7d0e_EOF'
+ cat << 'GH_AW_PROMPT_10c047efb98cf84f_EOF'
Tools: add_comment, missing_tool, missing_data, noop
- GH_AW_PROMPT_6f6003e03d2c7d0e_EOF
+ GH_AW_PROMPT_10c047efb98cf84f_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
- cat << 'GH_AW_PROMPT_6f6003e03d2c7d0e_EOF'
+ cat << 'GH_AW_PROMPT_10c047efb98cf84f_EOF'
The following GitHub context information is available for this workflow:
{{#if __GH_AW_GITHUB_ACTOR__ }}
@@ -285,9 +285,9 @@ jobs:
{{/if}}
- GH_AW_PROMPT_6f6003e03d2c7d0e_EOF
+ GH_AW_PROMPT_10c047efb98cf84f_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md"
- cat << 'GH_AW_PROMPT_6f6003e03d2c7d0e_EOF'
+ cat << 'GH_AW_PROMPT_10c047efb98cf84f_EOF'
@@ -334,7 +334,7 @@ jobs:
## Guidelines
- **Check memory first**: Skip any PR, issue, or release that already appears in the palace results from Step 0
- {{#if __GH_AW_EXPR_DD64DF46__ }}
+ {{#if __GH_AW_EXPR_CBA23170__ }}
- **Multi-candidate deliberation**: Before writing, identify exactly **3 distinct candidate facts** (one PR, one issue or release, one contributor or pattern). For each candidate write one sentence on why it is novel today. Then score each candidate 1–5 on: (a) novelty vs palace memory, (b) intrinsic poetic potential. Select the highest-scoring candidate and write the verse for that one only.
{{else}}
- **Favor recent updates** but include variety - pick something interesting, not just the most recent
@@ -390,7 +390,7 @@ jobs:
{{#runtime-import shared/noop-reminder.md}}
- GH_AW_PROMPT_6f6003e03d2c7d0e_EOF
+ GH_AW_PROMPT_10c047efb98cf84f_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -398,7 +398,7 @@ jobs:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_ENGINE_ID: "codex"
GH_AW_EXPERIMENTS_REASONING_DEPTH: ${{ steps.pick-experiment.outputs.reasoning_depth }}
- GH_AW_EXPR_DD64DF46: ${{ steps.pick-experiment.outputs.reasoning_depth == "multi_candidate" }}
+ GH_AW_EXPR_CBA23170: ${{ steps.pick-experiment.outputs.reasoning_depth == 'multi_candidate' }}
GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
with:
script: |
@@ -414,7 +414,7 @@ jobs:
GH_AW_CACHE_DESCRIPTION: ''
GH_AW_CACHE_DIR: '/tmp/gh-aw/cache-memory/'
GH_AW_EXPERIMENTS_REASONING_DEPTH: ${{ steps.pick-experiment.outputs.reasoning_depth }}
- GH_AW_EXPR_DD64DF46: ${{ steps.pick-experiment.outputs.reasoning_depth == "multi_candidate" }}
+ GH_AW_EXPR_CBA23170: ${{ steps.pick-experiment.outputs.reasoning_depth == 'multi_candidate' }}
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 }}
@@ -439,7 +439,7 @@ jobs:
GH_AW_CACHE_DESCRIPTION: process.env.GH_AW_CACHE_DESCRIPTION,
GH_AW_CACHE_DIR: process.env.GH_AW_CACHE_DIR,
GH_AW_EXPERIMENTS_REASONING_DEPTH: process.env.GH_AW_EXPERIMENTS_REASONING_DEPTH,
- GH_AW_EXPR_DD64DF46: process.env.GH_AW_EXPR_DD64DF46,
+ GH_AW_EXPR_CBA23170: process.env.GH_AW_EXPR_CBA23170,
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,
@@ -645,9 +645,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_d1209a06df4079df_EOF'
+ cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_65f0ad3627110281_EOF'
{"add_comment":{"max":1,"target":"4750"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
- GH_AW_SAFE_OUTPUTS_CONFIG_d1209a06df4079df_EOF
+ GH_AW_SAFE_OUTPUTS_CONFIG_65f0ad3627110281_EOF
- name: Generate Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
@@ -839,7 +839,7 @@ jobs:
DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0')
export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e CODEX_HOME -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6'
- cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_ece71de4bad38321_EOF
+ cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_c987b61d13852d08_EOF
[history]
persistence = "none"
@@ -866,11 +866,11 @@ jobs:
[mcp_servers.safeoutputs."guard-policies".write-sink]
accept = ["*"]
- GH_AW_MCP_CONFIG_ece71de4bad38321_EOF
+ GH_AW_MCP_CONFIG_c987b61d13852d08_EOF
# Generate JSON config for MCP gateway
GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
- cat << GH_AW_MCP_CONFIG_ece71de4bad38321_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
+ cat << GH_AW_MCP_CONFIG_c987b61d13852d08_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
{
"mcpServers": {
"mempalace": {
@@ -933,11 +933,11 @@ jobs:
}
}
}
- GH_AW_MCP_CONFIG_ece71de4bad38321_EOF
+ GH_AW_MCP_CONFIG_c987b61d13852d08_EOF
# Sync converter output to writable CODEX_HOME for Codex
mkdir -p /tmp/gh-aw/mcp-config
- cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_50e3008d43a21862_EOF
+ cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_1f630f4fb2369cb5_EOF
model_provider = "openai-proxy"
@@ -949,7 +949,7 @@ jobs:
[shell_environment_policy]
inherit = "core"
include_only = ["CODEX_API_KEY", "GH_AW_ASSETS_ALLOWED_EXTS", "GH_AW_ASSETS_BRANCH", "GH_AW_ASSETS_MAX_SIZE_KB", "GH_AW_SAFE_OUTPUTS", "GITHUB_REPOSITORY", "GITHUB_SERVER_URL", "HOME", "OPENAI_API_KEY", "PATH"]
- GH_AW_CODEX_SHELL_POLICY_50e3008d43a21862_EOF
+ GH_AW_CODEX_SHELL_POLICY_1f630f4fb2369cb5_EOF
awk '
BEGIN { skip_openai_proxy = 0 }
/^[[:space:]]*model_provider[[:space:]]*=/ { next }
@@ -1516,18 +1516,18 @@ jobs:
DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0')
export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e CODEX_HOME -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6'
- cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_14dbc03d40ba732e_EOF
+ cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_6abeefb5f2f72534_EOF
[history]
persistence = "none"
[shell_environment_policy]
inherit = "core"
include_only = ["CODEX_API_KEY", "HOME", "OPENAI_API_KEY", "PATH"]
- GH_AW_MCP_CONFIG_14dbc03d40ba732e_EOF
+ GH_AW_MCP_CONFIG_6abeefb5f2f72534_EOF
# Generate JSON config for MCP gateway
GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
- cat << GH_AW_MCP_CONFIG_d930ecb7f080feb5_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
+ cat << GH_AW_MCP_CONFIG_ab2f2995c9ec081d_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
{
"mcpServers": {
},
@@ -1538,11 +1538,11 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
- GH_AW_MCP_CONFIG_d930ecb7f080feb5_EOF
+ GH_AW_MCP_CONFIG_ab2f2995c9ec081d_EOF
# Sync converter output to writable CODEX_HOME for Codex
mkdir -p /tmp/gh-aw/mcp-config
- cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_9d95872585821d9c_EOF
+ cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_f08b56c0f509b97a_EOF
model_provider = "openai-proxy"
[model_providers.openai-proxy]
name = "OpenAI AWF proxy"
@@ -1552,7 +1552,7 @@ jobs:
[shell_environment_policy]
inherit = "core"
include_only = ["CODEX_API_KEY", "HOME", "OPENAI_API_KEY", "PATH"]
- GH_AW_CODEX_SHELL_POLICY_9d95872585821d9c_EOF
+ GH_AW_CODEX_SHELL_POLICY_f08b56c0f509b97a_EOF
awk '
BEGIN { skip_openai_proxy = 0 }
/^[[:space:]]*model_provider[[:space:]]*=/ { next }
diff --git a/.github/workflows/daily-fact.md b/.github/workflows/daily-fact.md
index 7574d87991c..320f5892276 100644
--- a/.github/workflows/daily-fact.md
+++ b/.github/workflows/daily-fact.md
@@ -99,7 +99,7 @@ Mine recent activity from the repository to find interesting facts. Focus on:
## Guidelines
- **Check memory first**: Skip any PR, issue, or release that already appears in the palace results from Step 0
-{{#if experiments.reasoning_depth == "multi_candidate"}}
+{{#if experiments.reasoning_depth == 'multi_candidate'}}
- **Multi-candidate deliberation**: Before writing, identify exactly **3 distinct candidate facts** (one PR, one issue or release, one contributor or pattern). For each candidate write one sentence on why it is novel today. Then score each candidate 1–5 on: (a) novelty vs palace memory, (b) intrinsic poetic potential. Select the highest-scoring candidate and write the verse for that one only.
{{else}}
- **Favor recent updates** but include variety - pick something interesting, not just the most recent
diff --git a/pkg/workflow/compiler_orchestrator_frontmatter.go b/pkg/workflow/compiler_orchestrator_frontmatter.go
index d77d58841a3..8450c2273f9 100644
--- a/pkg/workflow/compiler_orchestrator_frontmatter.go
+++ b/pkg/workflow/compiler_orchestrator_frontmatter.go
@@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
+ "github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/parser"
)
@@ -141,6 +142,15 @@ func (c *Compiler) parseFrontmatterSection(markdownPath string) (*frontmatterPar
return nil, fmt.Errorf("template condition validation failed: %w", err)
}
+ // Warn when experiment comparison expressions use double-quoted string literals.
+ // GitHub Actions expression syntax only supports single-quoted string literals, so
+ // the compiler converts double quotes to single quotes automatically — but authors
+ // should fix the source to use single quotes to keep it consistent with the output.
+ for _, w := range detectDoubleQuotedExperimentComparisons(result.Markdown) {
+ fmt.Fprintln(os.Stderr, console.FormatWarningMessage(w))
+ c.IncrementWarningCount()
+ }
+
log.Printf("Frontmatter: %d chars, Markdown: %d chars", len(result.Frontmatter), len(result.Markdown))
return &frontmatterParseResult{
diff --git a/pkg/workflow/compiler_template_validation_test.go b/pkg/workflow/compiler_template_validation_test.go
index 94601b699c2..5de3d4c2d7c 100644
--- a/pkg/workflow/compiler_template_validation_test.go
+++ b/pkg/workflow/compiler_template_validation_test.go
@@ -150,7 +150,7 @@ permissions:
strict: false
---
-{{#if experiments.prompt_style == "detailed"}}
+{{#if experiments.prompt_style == 'detailed'}}
detailed prompt
{{else}}
concise prompt
diff --git a/pkg/workflow/expression_extraction.go b/pkg/workflow/expression_extraction.go
index 00b92265f1c..b43393e661b 100644
--- a/pkg/workflow/expression_extraction.go
+++ b/pkg/workflow/expression_extraction.go
@@ -201,11 +201,11 @@ var experimentNameRegex = regexp.MustCompile(`^experiments\.([a-zA-Z_][a-zA-Z0-9
// experimentComparisonRegex matches experiments. followed by a comparison operator and
// a quoted string value, e.g. `experiments.prompt_style == "concise"` or
-// `experiments.prompt_style !== "detailed"`. The value must be enclosed in double quotes
-// with no embedded quotes. It captures:
+// `experiments.prompt_style !== "detailed"`. The value may be enclosed in double quotes or
+// single quotes, with no embedded quotes of the same kind. It captures:
// - group 1: the experiment name
// - group 2: the remainder of the expression (operator + quoted value), verbatim
-var experimentComparisonRegex = regexp.MustCompile(`^experiments\.([a-zA-Z_][a-zA-Z0-9_]*)([ \t]*(?:!==?|===?)[ \t]*"[^"]*"[ \t]*)$`)
+var experimentComparisonRegex = regexp.MustCompile(`^experiments\.([a-zA-Z_][a-zA-Z0-9_]*)([ \t]*(?:!==?|===?)[ \t]*(?:"[^"]*"|'[^']*')[ \t]*)$`)
// ExperimentEnvVarName returns the env-var name used for the given experiment.
// The name is uppercased; hyphens are converted to underscores; all other characters
@@ -221,7 +221,10 @@ func ExperimentEnvVarName(experimentName string) string {
// placeholder substitution step reads the value from the pick_experiment step output.
//
// Simple form: experiments.name → steps.pick-experiment.outputs.name
-// Comparison form: experiments.name == "v" → steps.pick-experiment.outputs.name == "v"
+// Comparison form: experiments.name == "v" → steps.pick-experiment.outputs.name == 'v'
+//
+// Double quotes in the comparison value are converted to single quotes because GitHub
+// Actions expression syntax only supports single-quoted string literals.
//
// This is used for ${{ experiments.name }} and ${{ experiments.name == "value" }} expressions
// that appear directly in the prompt body (mostly relevant in inline mode; in runtime-import
@@ -236,7 +239,13 @@ func transformExperimentsExpression(expr string) string {
return "steps.pick-experiment.outputs." + m[1]
}
if m := experimentComparisonRegex.FindStringSubmatch(expr); m != nil {
- return "steps.pick-experiment.outputs." + m[1] + m[2]
+ // Convert double quotes to single quotes: GitHub Actions expressions only
+ // support single-quoted string literals, not double-quoted ones.
+ // This replacement is safe because experimentComparisonRegex guarantees
+ // that quotes only appear as delimiters around the string literal value;
+ // no embedded quotes of the same kind are allowed by the pattern.
+ remainder := strings.ReplaceAll(m[2], `"`, `'`)
+ return "steps.pick-experiment.outputs." + m[1] + remainder
}
return expr
}
diff --git a/pkg/workflow/expression_extraction_test.go b/pkg/workflow/expression_extraction_test.go
index aebb2a2ba1f..912fb8cd0d1 100644
--- a/pkg/workflow/expression_extraction_test.go
+++ b/pkg/workflow/expression_extraction_test.go
@@ -65,25 +65,31 @@ func TestExpressionExtractor_ExtractExpressions(t *testing.T) {
name: "experiments.name == value comparison form gets transformed to step output",
markdown: `{{#if ${{ experiments.prompt_style == "concise" }} }}foo{{/if}}`,
wantCount: 1,
- wantExpressions: []string{`steps.pick-experiment.outputs.prompt_style == "concise"`},
+ wantExpressions: []string{`steps.pick-experiment.outputs.prompt_style == 'concise'`},
},
{
name: "experiments.name === value strict-equality form gets transformed to step output",
markdown: `{{#if ${{ experiments.prompt_style === "concise" }} }}foo{{/if}}`,
wantCount: 1,
- wantExpressions: []string{`steps.pick-experiment.outputs.prompt_style === "concise"`},
+ wantExpressions: []string{`steps.pick-experiment.outputs.prompt_style === 'concise'`},
},
{
name: "experiments.name != value inequality form gets transformed to step output",
markdown: `{{#if ${{ experiments.reasoning_depth != "multi_candidate" }} }}foo{{/if}}`,
wantCount: 1,
- wantExpressions: []string{`steps.pick-experiment.outputs.reasoning_depth != "multi_candidate"`},
+ wantExpressions: []string{`steps.pick-experiment.outputs.reasoning_depth != 'multi_candidate'`},
},
{
name: "experiments.name !== value strict-inequality form gets transformed to step output",
markdown: `{{#if ${{ experiments.reasoning_depth !== "multi_candidate" }} }}foo{{/if}}`,
wantCount: 1,
- wantExpressions: []string{`steps.pick-experiment.outputs.reasoning_depth !== "multi_candidate"`},
+ wantExpressions: []string{`steps.pick-experiment.outputs.reasoning_depth !== 'multi_candidate'`},
+ },
+ {
+ name: "experiments.name == value with single quotes gets transformed to step output",
+ markdown: `{{#if ${{ experiments.prompt_style == 'concise' }} }}foo{{/if}}`,
+ wantCount: 1,
+ wantExpressions: []string{`steps.pick-experiment.outputs.prompt_style == 'concise'`},
},
{
name: "expression with whitespace",
diff --git a/pkg/workflow/template_include_validation_test.go b/pkg/workflow/template_include_validation_test.go
index a1d4231aad2..e7282020420 100644
--- a/pkg/workflow/template_include_validation_test.go
+++ b/pkg/workflow/template_include_validation_test.go
@@ -532,3 +532,79 @@ func TestValidateNoIncludesInTemplateRegions_SingleError(t *testing.T) {
t.Errorf("Error should contain violation: tools.md")
}
}
+
+func TestDetectDoubleQuotedExperimentComparisons(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ wantWarnings int
+ wantContains string
+ }{
+ {
+ name: "no experiment expressions - no warning",
+ input: `{{#if github.event.issue.number}}content{{/if}}`,
+ wantWarnings: 0,
+ },
+ {
+ name: "experiment with single quotes - no warning",
+ input: `{{#if experiments.reasoning_depth == 'multi_candidate'}}content{{/if}}`,
+ wantWarnings: 0,
+ },
+ {
+ name: "simple experiment name (no comparison) - no warning",
+ input: `{{#if experiments.feature_flag}}content{{/if}}`,
+ wantWarnings: 0,
+ },
+ {
+ name: "experiment with double-quoted value - warning",
+ input: `{{#if experiments.reasoning_depth == "multi_candidate"}}content{{/if}}`,
+ wantWarnings: 1,
+ wantContains: "reasoning_depth",
+ },
+ {
+ name: "experiment with != and double quotes - warning",
+ input: `{{#if experiments.mode != "fast"}}content{{/if}}`,
+ wantWarnings: 1,
+ wantContains: "experiments.mode",
+ },
+ {
+ name: "experiment with !== and double quotes - warning",
+ input: `{{#if experiments.mode !== "fast"}}content{{/if}}`,
+ wantWarnings: 1,
+ wantContains: "experiments.mode",
+ },
+ {
+ name: "elseif with double-quoted value - warning",
+ input: `{{#if false}}a{{#elseif experiments.style == "detailed"}}b{{/if}}`,
+ wantWarnings: 1,
+ wantContains: "experiments.style",
+ },
+ {
+ name: "multiple occurrences - multiple warnings",
+ input: "{{#if experiments.a == \"x\"}}a{{/if}}\n{{#if experiments.b == \"y\"}}b{{/if}}",
+ wantWarnings: 2,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ warnings := detectDoubleQuotedExperimentComparisons(tt.input)
+ if len(warnings) != tt.wantWarnings {
+ t.Errorf("detectDoubleQuotedExperimentComparisons() = %d warning(s), want %d; got: %v",
+ len(warnings), tt.wantWarnings, warnings)
+ }
+ if tt.wantContains != "" {
+ found := false
+ for _, w := range warnings {
+ if strings.Contains(w, tt.wantContains) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("detectDoubleQuotedExperimentComparisons() warnings %v do not contain %q", warnings, tt.wantContains)
+ }
+ }
+ })
+ }
+}
diff --git a/pkg/workflow/template_validation.go b/pkg/workflow/template_validation.go
index f0e5903980a..edad80d4ce8 100644
--- a/pkg/workflow/template_validation.go
+++ b/pkg/workflow/template_validation.go
@@ -51,6 +51,12 @@ var (
// that is produced by the runtime and must never be written manually in workflow markdown.
// Authors should use the experiments. form (e.g. experiments.prompt_style == "detailed").
preExpandedExperimentPattern = regexp.MustCompile(`__GH_AW_EXPERIMENTS_[A-Z0-9_]+__`)
+
+ // experimentDoubleQuotePattern matches experiments. comparison expressions that use
+ // double-quoted string literals (e.g. experiments.mode == "value"). GitHub Actions
+ // expression syntax only supports single-quoted string literals, so double quotes must be
+ // replaced with single quotes before the expression reaches the lock file.
+ experimentDoubleQuotePattern = regexp.MustCompile(`experiments\.[a-zA-Z_][a-zA-Z0-9_]*\s*(?:!==?|===?)\s*"[^"]*"`)
)
// validateNoIncludesInTemplateRegions checks that import directives
@@ -130,3 +136,39 @@ func validateNoPreExpandedExperimentPlaceholders(markdown string) error {
return nil
}
+
+// detectDoubleQuotedExperimentComparisons scans template conditions for experiment comparison
+// expressions that use double-quoted string literals (e.g. experiments.mode == "value").
+// GitHub Actions expression syntax only supports single-quoted string literals, so double
+// quotes must be replaced with single quotes (e.g. experiments.mode == 'value').
+//
+// The compiler converts double quotes to single quotes automatically, but callers should
+// surface these findings as warnings so authors are prompted to fix the source.
+//
+// Returns one message per occurrence found, or nil if none.
+func detectDoubleQuotedExperimentComparisons(markdown string) []string {
+ templateValidationLog.Print("Checking for double-quoted experiment comparison expressions")
+
+ ifConditions := TemplateIfPattern.FindAllStringSubmatch(markdown, -1)
+ elseifConditions := TemplateElseIfPattern.FindAllStringSubmatch(markdown, -1)
+ allConditions := append(ifConditions, elseifConditions...)
+
+ var warnings []string
+ for _, m := range allConditions {
+ if len(m) < 2 {
+ continue
+ }
+ condition := strings.TrimSpace(m[1])
+ if match := experimentDoubleQuotePattern.FindString(condition); match != "" {
+ warnings = append(warnings, fmt.Sprintf(
+ "experiment comparison expression uses double quotes: %q — "+
+ "GitHub Actions expressions require single quotes; use single quotes instead "+
+ "(e.g. experiments.name == 'value')",
+ match,
+ ))
+ }
+ }
+
+ templateValidationLog.Printf("Found %d double-quoted experiment comparison(s)", len(warnings))
+ return warnings
+}