githubnext / githubnext/ado-aw

feat: decouple prompt body from compiled pipeline output (runtime-import approach)

Open
#237 1 comment 0 reactions 0 assignees View on GitHub
agentic-workflows enhancement rust
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.