Skip to content

feat: error when agent-job step outputs are referenced in the prompt body - #48610

Merged
pelikhan merged 6 commits into
mainfrom
copilot/detect-steps-outputs-in-prompt
Jul 28, 2026
Merged

feat: error when agent-job step outputs are referenced in the prompt body#48610
pelikhan merged 6 commits into
mainfrom
copilot/detect-steps-outputs-in-prompt

Conversation

Copilot AI commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

The prompt is rendered in the activation job; steps: / pre-steps: / pre-agent-steps: / post-steps: run in the agent job (after activation). Referencing their outputs in the prompt body silently resolves to empty. Steps should write results to files instead.

Changes

  • steps_output_in_prompt_validation.go — new validator called from validateExpressions:

    • Collects step id values from all four agent-job step YAML sections
    • Scans MarkdownContent for ${{ steps.STEP_ID.* }} where STEP_ID matches a collected agent-job step ID
    • Errors with an actionable message pointing to the file-based alternative
  • compiler_validators.go — wires validateStepsOutputsNotInPrompt into the existing validateExpressions gate

  • steps_output_in_prompt_validation_test.go — unit tests + full compilation pipeline tests

Example

# WRONG — steps.compute runs in the agent job, after the prompt is built
steps:
  - id: compute
    run: echo "value=42" >> $GITHUB_OUTPUT
---
Process this: ${{ steps.compute.outputs.value }}  # silently empty

# RIGHT — write to a file; reference the path in the prompt
steps:
  - id: compute
    run: echo "42" > /tmp/gh-aw/agent/result.txt
---
Read the result from /tmp/gh-aw/agent/result.txt.

Built-in activation-job steps (steps.sanitized.outputs.text etc.) are not flagged — they don't appear in user-visible step lists and are valid in the prompt.


Generated by 👨‍🍳 PR Sous Chef · gpt54 · 12.3 AIC · ⌖ 5.94 AIC · ⊞ 7.2K ·
Comment /souschef to run again

Add a new compile-time validation that errors when a workflow's markdown
body (the agent prompt) references ${{ steps.STEP_ID.* }} outputs from
steps defined in the frontmatter steps:/pre-steps:/pre-agent-steps:/
post-steps: sections.

The prompt is rendered in the activation job; those steps execute in the
agent job (after activation), so their outputs are unavailable when the
prompt is assembled. The correct pattern is to write results to files
under /tmp/gh-aw/agent/ and reference those file paths in the prompt.

Built-in activation-job steps (e.g. steps.sanitized.outputs.text) are
not flagged because they are not declared in the user-visible step lists.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title feat: detect agent-job step outputs referenced in prompt body feat: error when agent-job step outputs are referenced in the prompt body Jul 28, 2026
Copilot AI requested a review from pelikhan July 28, 2026 12:36
@pelikhan
pelikhan marked this pull request as ready for review July 28, 2026 12:37
Copilot AI review requested due to automatic review settings July 28, 2026 12:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds compile-time validation to prevent prompts from referencing outputs of later-running agent-job steps.

Changes:

  • Detects agent-job step references in prompt expressions.
  • Integrates validation into expression checks.
  • Adds unit and compilation-pipeline tests.
Show a summary per file
File Description
pkg/workflow/steps_output_in_prompt_validation.go Implements step-reference validation.
pkg/workflow/steps_output_in_prompt_validation_test.go Tests validation and compiler integration.
pkg/workflow/compiler_validators.go Wires validation into compilation.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (3)

pkg/workflow/steps_output_in_prompt_validation.go:36

  • The fast path is stricter than the regex and skips valid expressions with more than one space after {{, for example ${{ steps.compute.outputs.value }}. That expression passes the existing expression parser but this validator returns before checking it. Use the compiled matcher for this precheck.
	if !strings.Contains(workflowData.MarkdownContent, "${{ steps.") {

pkg/workflow/steps_output_in_prompt_validation.go:55

  • Step IDs are job-scoped, so membership in the agent-job ID set does not prove this reference resolves to that step. For example, a custom agent step may legally use id: sanitized, while ${{ steps.sanitized.outputs.text }} in the prompt resolves to the activation step defined in compiler_activation_steps.go:255; this check would reject that valid built-in reference. Activation-job IDs need to take precedence when classifying prompt references.
		if _, isAgentStep := agentStepIDs[stepID]; isAgentStep {

pkg/workflow/steps_output_in_prompt_validation.go:78

  • This recovery guidance is incorrect for IDs from post-steps: those steps run after the agent itself, so writing their result to a file still cannot make it available when the prompt is processed. The message should tell users to move any prompt input computation before the agent, then write it to a file.
		"Write step results to a file (e.g. /tmp/gh-aw/agent/result.txt) and reference that file path in the prompt instead of using ${{ steps.STEP_ID.outputs.* }}.",
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Medium


// stepsRefInPromptPattern matches ${{ steps.STEP_ID ... }} occurrences and
// captures the step ID so it can be compared against the agent-job step list.
var stepsRefInPromptPattern = regexp.MustCompile(`\$\{\{\s*steps\.([a-zA-Z0-9_-]+)`)
@github-actions github-actions Bot mentioned this pull request Jul 28, 2026
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat: error when agent-job step outputs are referenced in the prompt body

The implementation is clean and well-tested. The only material issue is already captured in the existing inline comment at line 22:

Regex captures only the first steps.* reference per expression. In an expression like ${{ steps.sanitized.outputs.text || steps.compute.outputs.value }}, only sanitized is captured. If compute is an agent-job step, the validation silently misses it.

No other blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 14.6 AIC · ⌖ 5.63 AIC · ⊞ 5K

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 3 test(s): 3 design, 0 implementation, 0 violation(s).

📊 Metrics (3 tests)
Metric Value
Analyzed 3 (Go: 3, JS: 0)
✅ Design 3 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 3 (100%)
Duplicate clusters 0
Inflation No (2.0:1, within threshold)
🚨 Violations 0
Test File Classification Issues
TestValidateStepsOutputsNotInPrompt steps_output_in_prompt_validation_test.go:27 behavioral_contract, design_test, high_value None
TestExtractAgentJobStepIDs steps_output_in_prompt_validation_test.go:126 behavioral_contract, design_test, high_value None
TestValidateStepsOutputsNotInPromptViaCompiler steps_output_in_prompt_validation_test.go:200 behavioral_contract, design_test, high_value None

Verdict

passed. 0% implementation tests (threshold: 30%). All tests are table-driven with error coverage and descriptive assertion messages. Compiler integration test is a strong design signal.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 32.6 AIC · ⌖ 8.79 AIC · ⊞ 8.1K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%).

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (398 new lines in pkg/workflow/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/48610-validate-agent-step-output-in-prompt.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-48610: Compile-Time Validation of Agent-Job Step Outputs in Prompt Body

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 0042-use-postgresql.md for PR #42).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 39.9 AIC · ⌖ 12.9 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd — requesting changes for a missing test coverage gap.

📋 Key Themes & Highlights

Key Themes

  • Missing test case: preAgentSteps (the pre-agent-steps: section) is wired into extractAgentJobStepIDs but never exercised by any test — a regression dropping that section would go undetected.
  • Minor clarity opportunity: the fast-path early-exit at line 56 is correct but benefits from a one-line comment.

Positive Highlights

  • ✅ Excellent regex design: captures step ID without requiring the full .outputs. suffix, so it catches any steps.ID reference regardless of property.
  • ✅ Good false-positive handling: built-in activation-job steps (sanitized, etc.) are excluded because they never appear in user-visible step lists.
  • ✅ Thorough integration test (TestValidateStepsOutputsNotInPromptViaCompiler) that validates the validator is actually wired into the compilation pipeline.
  • ✅ Actionable error message with a concrete file-based alternative — a great example of user-facing error design.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 35.8 AIC · ⌖ 4.86 AIC · ⊞ 6.7K
Comment /matt to run again


// TestValidateStepsOutputsNotInPromptViaCompiler verifies that the validation
// is wired into the compilation pipeline and produces a compile error when a
// step output is referenced in the prompt body.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] preAgentSteps is never exercised — steps defined under pre-agent-steps: won't be caught if they are accidentally dropped from extractAgentJobStepIDs.

💡 Suggested addition

Add preAgentSteps to the IDs collected from multiple step sections case (or a dedicated subtest):

preAgentSteps: `pre-agent-steps:
  - name: Pre-agent
    id: pre_agent_step
    run: echo pre-agent
`,
expectedIDs: []string{"main_step", "pre_step", "post_step", "pre_agent_step"},

Without this a regression that drops PreAgentSteps from extractAgentJobStepIDs would pass undetected.

@copilot please address this.

}
stepID := match[1]
if _, isAgentStep := agentStepIDs[stepID]; isAgentStep {
if _, already := seen[stepID]; !already {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The early-exit fast path (!strings.Contains(..., "${{ steps."))) skips the check when no steps. expression appears in the prompt — but this means a workflow with an expression like ${{ needs.job.outputs.x }} and no steps string will silently pass even if step IDs exist in other sections. This is fine by design, but worth adding a comment so future readers don't second-guess the correctness.

💡 Suggestion

Add a short inline comment on that line explaining the intent:

// Fast path: no steps reference in prompt means nothing to validate.
if !strings.Contains(workflowData.MarkdownContent, "${{ steps.") {
    return nil
}

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Request Changes

Good feature intent, but the fast-path guard is inconsistent with the regex it gates, causing a false negative for a common valid syntax variant — reintroducing exactly the bug class this PR aims to prevent.

💡 Themes
  • Blocking: strings.Contains(..., "${{ steps.") requires exactly one space; the regex allows zero-or-more (\s*). Confirmed locally that ${{steps.x.outputs.y}} (no space) bypasses the guard entirely and is never checked.
  • Non-blocking but worth considering: the regex scans raw markdown including fenced code blocks/inline code, so documentation-style examples of the anti-pattern would trigger false-positive compile errors with no escape hatch.
  • Test suite is decent but doesn't cover the no-space syntax variant, code-fence content, or || compound expressions — these gaps let the guard bug slip through.

Once the guard is aligned with the regex (or removed in favor of a single MatchString check), this looks solid.

🔎 Code quality review by PR Code Quality Reviewer · sonnet50 · 62.4 AIC · ⌖ 4.46 AIC · ⊞ 7.4K
Comment /review to run again

if workflowData == nil || workflowData.MarkdownContent == "" {
return nil
}
if !strings.Contains(workflowData.MarkdownContent, "${{ steps.") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fast-path guard has a false-negative gap: it only matches a literal single space after ${{, but the regex it gates allows zero or more spaces (\s*), so ${{steps.foo.outputs.bar}} (no space — valid GitHub Actions syntax) silently bypasses this validator entirely.

💡 Details and fix

Verified locally: strings.Contains("${{steps.compute.outputs.value}}", "${{ steps.") returns false, while stepsRefInPromptPattern matches that same string fine (\s* allows zero spaces). This defeats the entire purpose of the validator for a common, perfectly valid expression form — the exact case this PR is trying to catch would slip through undetected if written without the space.

Fix: make the guard consistent with the regex, e.g. just check for "steps." substring, or better, drop the fast-path Contains check and rely solely on the regex (it's cheap enough), or use stepsRefInPromptPattern.MatchString(...) directly as the guard:

if !stepsRefInPromptPattern.MatchString(workflowData.MarkdownContent) {
    return nil
}

This also removes the maintenance burden of keeping two independent patterns in sync.

return nil
}

matches := stepsRefInPromptPattern.FindAllStringSubmatch(workflowData.MarkdownContent, -1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex scans the raw MarkdownContent, including fenced code blocks and inline code spans, so a workflow prompt that merely documents/demonstrates the anti-pattern (e.g. "don't write ${{ steps.compute.outputs.value }} in your prompt") will trip this validator as a hard compile error.

@github-actions

Copy link
Copy Markdown
Contributor

🎉 Great work on this compile-time validation improvement! This PR adds a crucial safeguard that catches a common footgun: authors trying to reference agent-job step outputs in prompts, which silently resolve to empty due to gh-aw's two-job architecture.

What I'm seeing:

  • ✅ Clear, actionable error messages with the offending step IDs and a direct link to the file-based alternative (/tmp/gh-aw/agent/result.txt)
  • ✅ Comprehensive test coverage (266 lines) including unit tests and full compilation pipeline tests
  • ✅ Well-integrated into the existing validateExpressions gate with minimal changes to compiler_validators.go (+7 lines)
  • ✅ Excellent ADR documenting context, decision rationale, and consequences
  • ✅ Focused change with no scope creep — only one concern addressed
  • ✅ No new external dependencies

This is ready for review and merge. Well done!

Generated by ✅ Contribution Check · sonnet50 · 65.5 AIC · ⌖ 14.5 AIC · ⊞ 8.3K ·

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Use a two-step approach: extract ${{ ... }} expression bodies first,
then find all steps.STEP_ID occurrences within each body. Previously the
regex anchored to ${{ so only the first operand in expressions like
${{ steps.a.outputs.x || steps.b.outputs.y }} was checked.

Add test case covering the multi-operand expression scenario.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Fixed in the latest commit. The regex now uses a two-step approach: first extract each ${{ ... }} expression body with stepsExprBodyPattern, then scan every body for all steps.STEP_ID occurrences with stepsRefInExprPattern. This ensures all operands in expressions like ${{ steps.sanitized.outputs.text || steps.compute.outputs.value }} are checked. A test case covering this scenario was added.

@pelikhan
pelikhan merged commit a381232 into main Jul 28, 2026
28 checks passed
@pelikhan
pelikhan deleted the copilot/detect-steps-outputs-in-prompt branch July 28, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants