githubnext / githubnext/ado-aw
feat: decouple prompt body from compiled pipeline output (runtime-import approach)
- Dominant language
- Rust
- Stars
- 23
- Forks
- 8
- Avg merge
- 4d 9h
- Merged PRs (30d)
- 22
Description
## Problem
Currently, ado-aw embeds the **full markdown body** (the agent prompt) directly into the generated pipeline YAML at compile time via the `{{ agent_content }}` placeholder (`src/compile/common.rs:2082`, `templates/base.yml:152-154`). This means **any edit to the prompt — even a typo fix — forces a pipeline recompilation** and produces a diff in the compiled YAML.
In contrast, [gh-aw](https://github.com/github/gh-aw) decouples prompt content from compiled output. Editing the markdown body in gh-aw does **not** change the compiled `.lock.yml` file.
## How gh-aw solves this
gh-aw uses a **runtime-import macro** pattern:
### 1. Compile-time: emit a file reference, not the content
Instead of inlining the prompt body, the compiler emits a `{{#runtime-import }}` macro in the generated workflow:
```go
// pkg/workflow/compiler_yaml.go — generatePrompt(), ~line 556
runtimeImportMacro := fmt.Sprintf("{{#runtime-import %s}}", workflowFilePath)
userPromptChunks = append(userPromptChunks, runtimeImportMacro)
```
The compiled `.lock.yml` contains:
```yaml
- name: Create prompt with built-in context
run: |
{
cat << 'PROMPT'
...system prompts via cat of separate files...
{{#runtime-import .github/workflows/my-agent.md}}
PROMPT
} > "$GH_AW_PROMPT"
```
### 2. Runtime: resolve macros from the checked-out repository
A subsequent workflow step runs `interpolate_prompt.cjs` which:
1. Reads the prompt file containing the unresolved macros
2. Calls `runtime_import.cjs` → `processRuntimeImport()` to read the `.md` file from `$GITHUB_WORKSPACE`
3. Strips the YAML front matter, removes XML comments, validates expressions
4. Replaces the `{{#runtime-import ...}}` macro with the actual markdown body
5. Writes the resolved prompt back
Key source: `actions/setup/js/runtime_import.cjs:742-870`, `actions/setup/js/interpolate_prompt.cjs`
### 3. FrontmatterHash excludes the body
gh-aw's `FrontmatterHash` (SHA-256, embedded in the `.lock.yml` header) only covers the frontmatter text, not the markdown body (`pkg/parser/frontmatter_hash.go:164-182`). Body content is only included in the hash when `inlined-imports: true` is explicitly set.
### 4. Built-in system prompts are also external files
System prompts (XPIA, safe-outputs guidance, temp folder instructions, etc.) are stored as separate `.md` files in `actions/setup/md/` and loaded via `cat` at runtime from `${RUNNER_TEMP}/gh-aw/prompts/` — they are not inlined in the compiled output either (`pkg/workflow/prompt_constants.go:13-34`).
## Benefits
1. **Faster iteration** — Edit the agent prompt, push, done. No recompile step.
2. **Cleaner diffs** — Prompt changes only touch the `.md` file, not the pipeline YAML. Easier code review.
3. **Separation of concerns** — Pipeline infrastructure (tools, schedule, network, permissions) changes independently from agent instructions.
4. **Lock file stability** — The compiled output is truly a "lock" of the pipeline shape, not a copy of the prompt content.
## How ado-aw could achieve the same
ado-aw already writes the agent markdown body to `agents/.md` alongside the compiled pipeline. The pipeline already knows the source path via `{{ source_path }}`. The approach would be:
### Option A: Read agent file at runtime (minimal change)
Replace the `{{ agent_content }}` heredoc in `templates/base.yml` with a step that reads the agent file from the checked-out repo at runtime:
```yaml
- bash: |
# Extract markdown body from agent source file (strip YAML front matter)
SOURCE_FILE="{{ source_path }}"
awk '/^---$/{if(++c==2){skip=0;next}else{skip=1;next}} !skip' "$SOURCE_FILE" \
> /tmp/awf-tools/agent-prompt.md
displayName: "Prepare agent prompt"
```
Changes required:
- **`templates/base.yml`**: Replace the `{{ agent_content }}` heredoc with a runtime file-read step
- **`src/compile/common.rs`**: Remove `("{{ agent_content }}", markdown_body)` from the replacements list
- **`src/compile/mod.rs` (`check_pipeline`)**: Update integrity check to only compare frontmatter-derived configuration, since the prompt is no longer in the pipeline YAML
- **Tests**: Update snapshot/integration tests that assert on embedded prompt content
### Option B: Keep writing `agents/.md` but reference it
The compiler already writes an agent file to `agents/`. The pipeline step could `cat` that file directly, avoiding the need to strip front matter at runtime (since the agent file is already just the body). This is the cleanest path since the file is already being generated.
### Considerations
- **Security**: The prompt is read from the checked-out repo at runtime, so a PR can modify agent behavior without changing the pipeline file. This is acceptable because all write operations go through safe-outputs regardless, and the threat analysis job runs independently. The `check` command would need updating.
- **Backward compatibility**: Existing compiled pipelines with embedded prompts would continue to work. New compilations would use the runtime approach.
- **`inlined-imports` escape hatch**: gh-aw provides `inlined-imports: true` for cases where compile-time embedding is explicitly desired (e.g., Wasm/browser contexts). ado-aw could offer a similar opt-in if needed.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.