From 26a79890950c2f934da82d0580fa83a142903b9f Mon Sep 17 00:00:00 2001 From: malpou Date: Sun, 22 Mar 2026 08:40:40 +0100 Subject: [PATCH 1/3] feat: add {{ context.X }} placeholders for ralph name, iteration, and max_iterations Ralphs can now access runtime metadata via {{ context.name }}, {{ context.iteration }}, and {{ context.max_iterations }} placeholders. These are resolved alongside existing command and arg placeholders in a single pass to prevent cross-contamination. Closes #14 Co-authored-by: Ralphify --- src/ralphify/_frontmatter.py | 1 + src/ralphify/_resolver.py | 14 ++++--- src/ralphify/engine.py | 21 ++++++++-- tests/test_engine.py | 74 ++++++++++++++++++++++++++++++++---- tests/test_resolver.py | 47 +++++++++++++++++++++++ 5 files changed, 139 insertions(+), 18 deletions(-) diff --git a/src/ralphify/_frontmatter.py b/src/ralphify/_frontmatter.py index 0d4f6f3a..c3ac58ba 100644 --- a/src/ralphify/_frontmatter.py +++ b/src/ralphify/_frontmatter.py @@ -25,6 +25,7 @@ FIELD_COMMANDS = "commands" FIELD_ARGS = "args" FIELD_CREDIT = "credit" +FIELD_CONTEXT = "context" # Sub-field names within each command mapping. CMD_FIELD_NAME = "name" diff --git a/src/ralphify/_resolver.py b/src/ralphify/_resolver.py index eac7eb88..116bf668 100644 --- a/src/ralphify/_resolver.py +++ b/src/ralphify/_resolver.py @@ -12,7 +12,7 @@ import re -from ralphify._frontmatter import CMD_NAME_RE, FIELD_ARGS, FIELD_COMMANDS +from ralphify._frontmatter import CMD_NAME_RE, FIELD_ARGS, FIELD_COMMANDS, FIELD_CONTEXT # Pattern matching ``{{ args. }}`` placeholders — used by resolve_args @@ -35,9 +35,9 @@ def _replace(match: re.Match) -> str: return _ARGS_PATTERN.sub(_replace, prompt) -# Single pattern matching both placeholder kinds for single-pass resolution. +# Single pattern matching all placeholder kinds for single-pass resolution. _ALL_PATTERN = re.compile( - rf"\{{\{{\s*({FIELD_COMMANDS}|{FIELD_ARGS})\.({CMD_NAME_RE.pattern})\s*\}}\}}" + rf"\{{\{{\s*({FIELD_COMMANDS}|{FIELD_ARGS}|{FIELD_CONTEXT})\.({CMD_NAME_RE.pattern})\s*\}}\}}" ) @@ -45,16 +45,18 @@ def resolve_all( prompt: str, command_outputs: dict[str, str], user_args: dict[str, str], + context: dict[str, str] | None = None, ) -> str: """Resolve all placeholders in a single pass to prevent cross-contamination. - Resolves both ``{{ commands.name }}`` and ``{{ args.name }}`` in a - single pass so values inserted by one kind of placeholder are not - re-processed as the other kind. + Resolves ``{{ commands.name }}``, ``{{ args.name }}``, and + ``{{ context.name }}`` in a single pass so values inserted by one + kind of placeholder are not re-processed as the other kind. """ lookups: dict[str, dict[str, str]] = { FIELD_COMMANDS: command_outputs, FIELD_ARGS: user_args, + FIELD_CONTEXT: context or {}, } def _replace(match: re.Match) -> str: diff --git a/src/ralphify/engine.py b/src/ralphify/engine.py index 897e6fc2..8680c471 100644 --- a/src/ralphify/engine.py +++ b/src/ralphify/engine.py @@ -120,18 +120,31 @@ def _run_commands( return results +def _build_context(config: RunConfig, state: RunState) -> dict[str, str]: + """Build the context dict for ``{{ context.X }}`` placeholders.""" + ctx: dict[str, str] = { + "name": config.ralph_dir.name, + "iteration": str(state.iteration), + } + if config.max_iterations is not None: + ctx["max_iterations"] = str(config.max_iterations) + return ctx + + def _assemble_prompt( config: RunConfig, + state: RunState, command_outputs: dict[str, str], ) -> str: """Build the full prompt for one iteration. - Reads the RALPH.md body, resolves user args and command output - placeholders. + Reads the RALPH.md body, resolves user args, command output, and + context placeholders. """ raw = config.ralph_file.read_text(encoding="utf-8") _, prompt = parse_frontmatter(raw) - prompt = resolve_all(prompt, command_outputs, config.args) + context = _build_context(config, state) + prompt = resolve_all(prompt, command_outputs, config.args, context) if config.credit: prompt += _CREDIT_INSTRUCTION return prompt @@ -227,7 +240,7 @@ def _run_iteration( )) # Assemble prompt - prompt = _assemble_prompt(config, command_outputs) + prompt = _assemble_prompt(config, state, command_outputs) emit( EventType.PROMPT_ASSEMBLED, PromptAssembledData(iteration=iteration, prompt_length=len(prompt)), diff --git a/tests/test_engine.py b/tests/test_engine.py index 29e7902a..d3136c81 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -796,8 +796,10 @@ class TestAssemblePrompt: def test_reads_prompt_from_ralph_file(self, tmp_path): config = make_config(tmp_path, "simple prompt", max_iterations=1, credit=False) + state = make_state() + state.iteration = 1 - result = _assemble_prompt(config, {}) + result = _assemble_prompt(config, state, {}) assert result == "simple prompt" @@ -809,8 +811,10 @@ def test_resolves_command_placeholders(self, tmp_path): max_iterations=1, credit=False, ) + state = make_state() + state.iteration = 1 - result = _assemble_prompt(config, {"tests": "all passed"}) + result = _assemble_prompt(config, state, {"tests": "all passed"}) assert result == "Results: all passed" @@ -822,37 +826,47 @@ def test_resolves_args_placeholders(self, tmp_path): args={"dir": "./src"}, credit=False, ) + state = make_state() + state.iteration = 1 - result = _assemble_prompt(config, {}) + result = _assemble_prompt(config, state, {}) assert result == "Search ./src" def test_clears_unresolved_placeholders(self, tmp_path): config = make_config(tmp_path, "Before {{ args.missing }} after", max_iterations=1, args={}, credit=False) + state = make_state() + state.iteration = 1 - result = _assemble_prompt(config, {}) + result = _assemble_prompt(config, state, {}) assert result == "Before after" def test_strips_html_comments(self, tmp_path): config = make_config(tmp_path, "Before after", max_iterations=1, credit=False) + state = make_state() + state.iteration = 1 - result = _assemble_prompt(config, {}) + result = _assemble_prompt(config, state, {}) assert result == "Before after" def test_credit_instruction_appended_by_default(self, tmp_path): config = make_config(tmp_path, "simple prompt", max_iterations=1) + state = make_state() + state.iteration = 1 - result = _assemble_prompt(config, {}) + result = _assemble_prompt(config, state, {}) assert result.startswith("simple prompt") assert "Co-authored-by: Ralphify " in result def test_credit_false_omits_instruction(self, tmp_path): config = make_config(tmp_path, "simple prompt", max_iterations=1, credit=False) + state = make_state() + state.iteration = 1 - result = _assemble_prompt(config, {}) + result = _assemble_prompt(config, state, {}) assert result == "simple prompt" @@ -869,12 +883,56 @@ def test_arg_values_not_resolved_as_command_placeholders(self, tmp_path): commands=[Command(name="tests", run="pytest")], credit=False, ) + state = make_state() + state.iteration = 1 - result = _assemble_prompt(config, {"tests": "5 passed"}) + result = _assemble_prompt(config, state, {"tests": "5 passed"}) assert "Filter: {{ commands.tests }}" in result assert "Tests: 5 passed" in result + def test_resolves_context_placeholders(self, tmp_path): + config = make_config( + tmp_path, + "Name: {{ context.name }}, Iter: {{ context.iteration }}, Max: {{ context.max_iterations }}", + max_iterations=5, + credit=False, + ) + state = make_state() + state.iteration = 3 + + result = _assemble_prompt(config, state, {}) + + assert result == "Name: my-ralph, Iter: 3, Max: 5" + + def test_context_max_iterations_empty_when_unlimited(self, tmp_path): + config = make_config( + tmp_path, + "Max: {{ context.max_iterations }}", + max_iterations=None, + credit=False, + ) + state = make_state() + state.iteration = 1 + + result = _assemble_prompt(config, state, {}) + + assert result == "Max: " + + def test_context_name_is_ralph_dir_name(self, tmp_path): + config = make_config( + tmp_path, + "Name: {{ context.name }}", + max_iterations=1, + credit=False, + ) + state = make_state() + state.iteration = 1 + + result = _assemble_prompt(config, state, {}) + + assert result == "Name: my-ralph" + class TestCreditInLoop: @patch(MOCK_SUBPROCESS, side_effect=ok_result) diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 0553c931..524409dc 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -152,3 +152,50 @@ def test_whitespace_tolerant(self): def test_hyphenated_arg_name(self): result = resolve_args("{{ args.my-dir }}", {"my-dir": "/tmp"}) assert result == "/tmp" + + +class TestResolveContext: + """Tests for {{ context.X }} placeholders passed through resolve_all.""" + + def test_resolves_context_name(self): + result = resolve_all("Ralph: {{ context.name }}", {}, {}, {"name": "my-ralph"}) + assert result == "Ralph: my-ralph" + + def test_resolves_context_iteration(self): + result = resolve_all("Iter: {{ context.iteration }}", {}, {}, {"iteration": "3"}) + assert result == "Iter: 3" + + def test_resolves_context_max_iterations(self): + result = resolve_all( + "Max: {{ context.max_iterations }}", {}, {}, {"max_iterations": "10"}, + ) + assert result == "Max: 10" + + def test_unknown_context_key_resolves_to_empty(self): + result = resolve_all("{{ context.unknown }}", {}, {}, {"name": "test"}) + assert result == "" + + def test_no_context_clears_placeholders(self): + result = resolve_all("{{ context.name }}", {}, {}) + assert result == "" + + def test_context_with_commands_and_args(self): + result = resolve_all( + "{{ commands.tests }} {{ args.dir }} {{ context.iteration }}", + {"tests": "ok"}, {"dir": "./src"}, {"iteration": "2"}, + ) + assert result == "ok ./src 2" + + def test_context_value_not_resolved_as_command_placeholder(self): + result = resolve_all( + "Ctx: {{ context.name }}\nCmd: {{ commands.tests }}", + {"tests": "5 passed"}, + {}, + {"name": "{{ commands.tests }}"}, + ) + assert "Ctx: {{ commands.tests }}" in result + assert "Cmd: 5 passed" in result + + def test_whitespace_tolerant(self): + result = resolve_all("{{ context.name }}", {}, {}, {"name": "test"}) + assert result == "test" From 84f3b875d4bbc32cc73c9dba7f8cdf61963090c6 Mon Sep 17 00:00:00 2001 From: malpou Date: Sun, 22 Mar 2026 08:42:13 +0100 Subject: [PATCH 2/3] docs: document context placeholders for name, iteration, and max_iterations Co-authored-by: Ralphify --- docs/changelog.md | 8 ++++++++ docs/contributing/codebase-map.md | 2 +- docs/quick-reference.md | 13 ++++++++++++- docs/writing-prompts.md | 1 + src/ralphify/skills/new-ralph/SKILL.md | 3 ++- 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 59a9bcd8..93f23a09 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,6 +8,14 @@ keywords: ralphify changelog, release history, new features, version updates, br All notable changes to ralphify are documented here. +## 0.2.5 — 2026-03-22 + +### Added + +- **Context placeholders** — ralphs can now access runtime metadata via `{{ context.name }}` (ralph directory name), `{{ context.iteration }}` (current iteration, 1-based), and `{{ context.max_iterations }}` (total iterations if `-n` was set, empty otherwise). No frontmatter configuration needed. + +--- + ## 0.2.4 — 2026-03-22 ### Fixed diff --git a/docs/contributing/codebase-map.md b/docs/contributing/codebase-map.md index 3e8b45c5..c743890a 100644 --- a/docs/contributing/codebase-map.md +++ b/docs/contributing/codebase-map.md @@ -22,7 +22,7 @@ src/ralphify/ # All source code ├── cli.py # CLI commands (run, new, init) — delegates to engine for the loop ├── engine.py # Core run loop orchestration with structured event emission ├── manager.py # Multi-run orchestration (concurrent runs via threads) -├── _resolver.py # Template placeholder resolution ({{ commands.* }}, {{ args.* }}) +├── _resolver.py # Template placeholder resolution ({{ commands.* }}, {{ args.* }}, {{ context.* }}) ├── _agent.py # Run agent subprocesses (streaming + blocking modes, log writing) ├── _run_types.py # RunConfig, RunState, RunStatus, Command — shared data types ├── _runner.py # Execute shell commands with timeout and capture output diff --git a/docs/quick-reference.md b/docs/quick-reference.md index 817ab554..9b10e700 100644 --- a/docs/quick-reference.md +++ b/docs/quick-reference.md @@ -107,13 +107,24 @@ Your instructions here. Use {{ args.dir }} for user arguments. - `--` ends flag parsing: `ralph run my-ralph -- --verbose ./src` treats `--verbose` as a positional value - Missing args resolve to empty string +### Context placeholders + +```markdown +{{ context.name }} # Ralph directory name (e.g. "my-ralph") +{{ context.iteration }} # Current iteration number (1-based) +{{ context.max_iterations }} # Total iterations if -n was set, empty otherwise +``` + +- Automatically available — no frontmatter configuration needed +- Useful for progress tracking, naming logs, or adjusting behavior near the end of a run + ## The loop Each iteration: 1. Re-read `RALPH.md` from disk 2. Run all commands in order, capture output -3. Resolve `{{ commands.* }}` and `{{ args.* }}` placeholders +3. Resolve `{{ commands.* }}`, `{{ args.* }}`, and `{{ context.* }}` placeholders 4. Pipe assembled prompt to agent via stdin 5. Wait for agent to exit 6. Repeat diff --git a/docs/writing-prompts.md b/docs/writing-prompts.md index 642b9fd6..6ba85490 100644 --- a/docs/writing-prompts.md +++ b/docs/writing-prompts.md @@ -341,6 +341,7 @@ Rules of thumb: - **Commands:** Pick the 2-3 most useful signals. Don't add commands whose output the agent doesn't need. - **Command output:** Can be long. If your commands produce verbose output, consider using scripts that filter to the relevant lines. - **User args:** Use `{{ args.name }}` to make ralphs reusable — pass project-specific values from the CLI instead of hardcoding them in the prompt. Args also work in command `run` strings (e.g., `run: gh issue view {{ args.issue }}`). +- **Context placeholders:** Use `{{ context.name }}`, `{{ context.iteration }}`, and `{{ context.max_iterations }}` to access runtime metadata — the ralph's directory name, which iteration this is, and the total number if `--n` was set. - **Working directory:** Commands run from the project root by default. Commands starting with `./` run from the ralph directory — handy for bundling helper scripts next to your `RALPH.md`. ## Next steps diff --git a/src/ralphify/skills/new-ralph/SKILL.md b/src/ralphify/skills/new-ralph/SKILL.md index 32e4f0ca..921dcdea 100644 --- a/src/ralphify/skills/new-ralph/SKILL.md +++ b/src/ralphify/skills/new-ralph/SKILL.md @@ -86,9 +86,10 @@ If any tests are failing above, fix them before continuing. #### Body -The body is the prompt. It supports two placeholder types: +The body is the prompt. It supports three placeholder types: - `{{ commands. }}` — replaced with command output each iteration - `{{ args. }}` — replaced with CLI arguments +- `{{ context. }}` — replaced with runtime metadata (`name`, `iteration`, `max_iterations`) HTML comments (``) are automatically stripped before the prompt is assembled. They never reach the agent. Use them for notes about why rules exist or TODOs for prompt maintenance. From d191e2dc442dd755aba14cee75c1dfbd641be6e9 Mon Sep 17 00:00:00 2001 From: Kasper Junge Date: Tue, 24 Mar 2026 13:22:48 +0100 Subject: [PATCH 3/3] refactor: rename context.* placeholders to ralph.* {{ ralph.name }}, {{ ralph.iteration }}, and {{ ralph.max_iterations }} read more naturally in RALPH.md files than the context.* prefix. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/changelog.md | 2 +- docs/contributing/codebase-map.md | 2 +- docs/quick-reference.md | 10 ++++---- docs/writing-prompts.md | 2 +- src/ralphify/_frontmatter.py | 2 +- src/ralphify/_resolver.py | 10 ++++---- src/ralphify/engine.py | 8 +++--- src/ralphify/skills/new-ralph/SKILL.md | 2 +- tests/test_engine.py | 12 ++++----- tests/test_resolver.py | 34 +++++++++++++------------- 10 files changed, 42 insertions(+), 42 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 93f23a09..2ff94dd5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,7 +12,7 @@ All notable changes to ralphify are documented here. ### Added -- **Context placeholders** — ralphs can now access runtime metadata via `{{ context.name }}` (ralph directory name), `{{ context.iteration }}` (current iteration, 1-based), and `{{ context.max_iterations }}` (total iterations if `-n` was set, empty otherwise). No frontmatter configuration needed. +- **Ralph placeholders** — ralphs can now access runtime metadata via `{{ ralph.name }}` (ralph directory name), `{{ ralph.iteration }}` (current iteration, 1-based), and `{{ ralph.max_iterations }}` (total iterations if `-n` was set, empty otherwise). No frontmatter configuration needed. --- diff --git a/docs/contributing/codebase-map.md b/docs/contributing/codebase-map.md index c743890a..e4647625 100644 --- a/docs/contributing/codebase-map.md +++ b/docs/contributing/codebase-map.md @@ -22,7 +22,7 @@ src/ralphify/ # All source code ├── cli.py # CLI commands (run, new, init) — delegates to engine for the loop ├── engine.py # Core run loop orchestration with structured event emission ├── manager.py # Multi-run orchestration (concurrent runs via threads) -├── _resolver.py # Template placeholder resolution ({{ commands.* }}, {{ args.* }}, {{ context.* }}) +├── _resolver.py # Template placeholder resolution ({{ commands.* }}, {{ args.* }}, {{ ralph.* }}) ├── _agent.py # Run agent subprocesses (streaming + blocking modes, log writing) ├── _run_types.py # RunConfig, RunState, RunStatus, Command — shared data types ├── _runner.py # Execute shell commands with timeout and capture output diff --git a/docs/quick-reference.md b/docs/quick-reference.md index 9b10e700..9e05ed11 100644 --- a/docs/quick-reference.md +++ b/docs/quick-reference.md @@ -107,12 +107,12 @@ Your instructions here. Use {{ args.dir }} for user arguments. - `--` ends flag parsing: `ralph run my-ralph -- --verbose ./src` treats `--verbose` as a positional value - Missing args resolve to empty string -### Context placeholders +### Ralph placeholders ```markdown -{{ context.name }} # Ralph directory name (e.g. "my-ralph") -{{ context.iteration }} # Current iteration number (1-based) -{{ context.max_iterations }} # Total iterations if -n was set, empty otherwise +{{ ralph.name }} # Ralph directory name (e.g. "my-ralph") +{{ ralph.iteration }} # Current iteration number (1-based) +{{ ralph.max_iterations }} # Total iterations if -n was set, empty otherwise ``` - Automatically available — no frontmatter configuration needed @@ -124,7 +124,7 @@ Each iteration: 1. Re-read `RALPH.md` from disk 2. Run all commands in order, capture output -3. Resolve `{{ commands.* }}`, `{{ args.* }}`, and `{{ context.* }}` placeholders +3. Resolve `{{ commands.* }}`, `{{ args.* }}`, and `{{ ralph.* }}` placeholders 4. Pipe assembled prompt to agent via stdin 5. Wait for agent to exit 6. Repeat diff --git a/docs/writing-prompts.md b/docs/writing-prompts.md index 6ba85490..dbdac72e 100644 --- a/docs/writing-prompts.md +++ b/docs/writing-prompts.md @@ -341,7 +341,7 @@ Rules of thumb: - **Commands:** Pick the 2-3 most useful signals. Don't add commands whose output the agent doesn't need. - **Command output:** Can be long. If your commands produce verbose output, consider using scripts that filter to the relevant lines. - **User args:** Use `{{ args.name }}` to make ralphs reusable — pass project-specific values from the CLI instead of hardcoding them in the prompt. Args also work in command `run` strings (e.g., `run: gh issue view {{ args.issue }}`). -- **Context placeholders:** Use `{{ context.name }}`, `{{ context.iteration }}`, and `{{ context.max_iterations }}` to access runtime metadata — the ralph's directory name, which iteration this is, and the total number if `--n` was set. +- **Ralph placeholders:** Use `{{ ralph.name }}`, `{{ ralph.iteration }}`, and `{{ ralph.max_iterations }}` to access runtime metadata — the ralph's directory name, which iteration this is, and the total number if `--n` was set. - **Working directory:** Commands run from the project root by default. Commands starting with `./` run from the ralph directory — handy for bundling helper scripts next to your `RALPH.md`. ## Next steps diff --git a/src/ralphify/_frontmatter.py b/src/ralphify/_frontmatter.py index c3ac58ba..7a4afb9a 100644 --- a/src/ralphify/_frontmatter.py +++ b/src/ralphify/_frontmatter.py @@ -25,7 +25,7 @@ FIELD_COMMANDS = "commands" FIELD_ARGS = "args" FIELD_CREDIT = "credit" -FIELD_CONTEXT = "context" +FIELD_RALPH = "ralph" # Sub-field names within each command mapping. CMD_FIELD_NAME = "name" diff --git a/src/ralphify/_resolver.py b/src/ralphify/_resolver.py index 116bf668..179f4ec3 100644 --- a/src/ralphify/_resolver.py +++ b/src/ralphify/_resolver.py @@ -12,7 +12,7 @@ import re -from ralphify._frontmatter import CMD_NAME_RE, FIELD_ARGS, FIELD_COMMANDS, FIELD_CONTEXT +from ralphify._frontmatter import CMD_NAME_RE, FIELD_ARGS, FIELD_COMMANDS, FIELD_RALPH # Pattern matching ``{{ args. }}`` placeholders — used by resolve_args @@ -37,7 +37,7 @@ def _replace(match: re.Match) -> str: # Single pattern matching all placeholder kinds for single-pass resolution. _ALL_PATTERN = re.compile( - rf"\{{\{{\s*({FIELD_COMMANDS}|{FIELD_ARGS}|{FIELD_CONTEXT})\.({CMD_NAME_RE.pattern})\s*\}}\}}" + rf"\{{\{{\s*({FIELD_COMMANDS}|{FIELD_ARGS}|{FIELD_RALPH})\.({CMD_NAME_RE.pattern})\s*\}}\}}" ) @@ -45,18 +45,18 @@ def resolve_all( prompt: str, command_outputs: dict[str, str], user_args: dict[str, str], - context: dict[str, str] | None = None, + ralph_context: dict[str, str] | None = None, ) -> str: """Resolve all placeholders in a single pass to prevent cross-contamination. Resolves ``{{ commands.name }}``, ``{{ args.name }}``, and - ``{{ context.name }}`` in a single pass so values inserted by one + ``{{ ralph.name }}`` in a single pass so values inserted by one kind of placeholder are not re-processed as the other kind. """ lookups: dict[str, dict[str, str]] = { FIELD_COMMANDS: command_outputs, FIELD_ARGS: user_args, - FIELD_CONTEXT: context or {}, + FIELD_RALPH: ralph_context or {}, } def _replace(match: re.Match) -> str: diff --git a/src/ralphify/engine.py b/src/ralphify/engine.py index 8680c471..c861c56e 100644 --- a/src/ralphify/engine.py +++ b/src/ralphify/engine.py @@ -120,8 +120,8 @@ def _run_commands( return results -def _build_context(config: RunConfig, state: RunState) -> dict[str, str]: - """Build the context dict for ``{{ context.X }}`` placeholders.""" +def _build_ralph_context(config: RunConfig, state: RunState) -> dict[str, str]: + """Build the context dict for ``{{ ralph.X }}`` placeholders.""" ctx: dict[str, str] = { "name": config.ralph_dir.name, "iteration": str(state.iteration), @@ -143,8 +143,8 @@ def _assemble_prompt( """ raw = config.ralph_file.read_text(encoding="utf-8") _, prompt = parse_frontmatter(raw) - context = _build_context(config, state) - prompt = resolve_all(prompt, command_outputs, config.args, context) + ralph_context = _build_ralph_context(config, state) + prompt = resolve_all(prompt, command_outputs, config.args, ralph_context) if config.credit: prompt += _CREDIT_INSTRUCTION return prompt diff --git a/src/ralphify/skills/new-ralph/SKILL.md b/src/ralphify/skills/new-ralph/SKILL.md index 921dcdea..96718cc9 100644 --- a/src/ralphify/skills/new-ralph/SKILL.md +++ b/src/ralphify/skills/new-ralph/SKILL.md @@ -89,7 +89,7 @@ If any tests are failing above, fix them before continuing. The body is the prompt. It supports three placeholder types: - `{{ commands. }}` — replaced with command output each iteration - `{{ args. }}` — replaced with CLI arguments -- `{{ context. }}` — replaced with runtime metadata (`name`, `iteration`, `max_iterations`) +- `{{ ralph. }}` — replaced with runtime metadata (`name`, `iteration`, `max_iterations`) HTML comments (``) are automatically stripped before the prompt is assembled. They never reach the agent. Use them for notes about why rules exist or TODOs for prompt maintenance. diff --git a/tests/test_engine.py b/tests/test_engine.py index d3136c81..1db12e2a 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -891,10 +891,10 @@ def test_arg_values_not_resolved_as_command_placeholders(self, tmp_path): assert "Filter: {{ commands.tests }}" in result assert "Tests: 5 passed" in result - def test_resolves_context_placeholders(self, tmp_path): + def test_resolves_ralph_placeholders(self, tmp_path): config = make_config( tmp_path, - "Name: {{ context.name }}, Iter: {{ context.iteration }}, Max: {{ context.max_iterations }}", + "Name: {{ ralph.name }}, Iter: {{ ralph.iteration }}, Max: {{ ralph.max_iterations }}", max_iterations=5, credit=False, ) @@ -905,10 +905,10 @@ def test_resolves_context_placeholders(self, tmp_path): assert result == "Name: my-ralph, Iter: 3, Max: 5" - def test_context_max_iterations_empty_when_unlimited(self, tmp_path): + def test_ralph_max_iterations_empty_when_unlimited(self, tmp_path): config = make_config( tmp_path, - "Max: {{ context.max_iterations }}", + "Max: {{ ralph.max_iterations }}", max_iterations=None, credit=False, ) @@ -919,10 +919,10 @@ def test_context_max_iterations_empty_when_unlimited(self, tmp_path): assert result == "Max: " - def test_context_name_is_ralph_dir_name(self, tmp_path): + def test_ralph_name_is_ralph_dir_name(self, tmp_path): config = make_config( tmp_path, - "Name: {{ context.name }}", + "Name: {{ ralph.name }}", max_iterations=1, credit=False, ) diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 524409dc..9f14bf4e 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -154,41 +154,41 @@ def test_hyphenated_arg_name(self): assert result == "/tmp" -class TestResolveContext: - """Tests for {{ context.X }} placeholders passed through resolve_all.""" +class TestResolveRalphContext: + """Tests for {{ ralph.X }} placeholders passed through resolve_all.""" - def test_resolves_context_name(self): - result = resolve_all("Ralph: {{ context.name }}", {}, {}, {"name": "my-ralph"}) + def test_resolves_ralph_name(self): + result = resolve_all("Ralph: {{ ralph.name }}", {}, {}, {"name": "my-ralph"}) assert result == "Ralph: my-ralph" - def test_resolves_context_iteration(self): - result = resolve_all("Iter: {{ context.iteration }}", {}, {}, {"iteration": "3"}) + def test_resolves_ralph_iteration(self): + result = resolve_all("Iter: {{ ralph.iteration }}", {}, {}, {"iteration": "3"}) assert result == "Iter: 3" - def test_resolves_context_max_iterations(self): + def test_resolves_ralph_max_iterations(self): result = resolve_all( - "Max: {{ context.max_iterations }}", {}, {}, {"max_iterations": "10"}, + "Max: {{ ralph.max_iterations }}", {}, {}, {"max_iterations": "10"}, ) assert result == "Max: 10" - def test_unknown_context_key_resolves_to_empty(self): - result = resolve_all("{{ context.unknown }}", {}, {}, {"name": "test"}) + def test_unknown_ralph_key_resolves_to_empty(self): + result = resolve_all("{{ ralph.unknown }}", {}, {}, {"name": "test"}) assert result == "" - def test_no_context_clears_placeholders(self): - result = resolve_all("{{ context.name }}", {}, {}) + def test_no_ralph_context_clears_placeholders(self): + result = resolve_all("{{ ralph.name }}", {}, {}) assert result == "" - def test_context_with_commands_and_args(self): + def test_ralph_with_commands_and_args(self): result = resolve_all( - "{{ commands.tests }} {{ args.dir }} {{ context.iteration }}", + "{{ commands.tests }} {{ args.dir }} {{ ralph.iteration }}", {"tests": "ok"}, {"dir": "./src"}, {"iteration": "2"}, ) assert result == "ok ./src 2" - def test_context_value_not_resolved_as_command_placeholder(self): + def test_ralph_value_not_resolved_as_command_placeholder(self): result = resolve_all( - "Ctx: {{ context.name }}\nCmd: {{ commands.tests }}", + "Ctx: {{ ralph.name }}\nCmd: {{ commands.tests }}", {"tests": "5 passed"}, {}, {"name": "{{ commands.tests }}"}, @@ -197,5 +197,5 @@ def test_context_value_not_resolved_as_command_placeholder(self): assert "Cmd: 5 passed" in result def test_whitespace_tolerant(self): - result = resolve_all("{{ context.name }}", {}, {}, {"name": "test"}) + result = resolve_all("{{ ralph.name }}", {}, {}, {"name": "test"}) assert result == "test"