Skip to content

Commit 1c0b938

Browse files
Kasper Jungeclaude
authored andcommitted
fix: reject unknown ralph names instead of silently treating them as inline prompts
Previously `ralph run nonexistent` would pass the literal string "nonexistent" to the agent as an inline prompt. Now the positional argument must be a named ralph from .ralphify/ralphs/ or omitted entirely (falls back to ralph.toml). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent de1de4b commit 1c0b938

3 files changed

Lines changed: 29 additions & 77 deletions

File tree

src/ralphify/cli.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def status() -> None:
224224

225225
@app.command()
226226
def run(
227-
prompt: str | None = typer.Argument(None, help="Ralph name, file path, or inline prompt text."),
227+
prompt: str | None = typer.Argument(None, help="Named ralph from .ralphify/ralphs/."),
228228
n: int | None = typer.Option(None, "-n", help="Max number of iterations. Infinite if not set."),
229229
stop_on_error: bool = typer.Option(False, "--stop-on-error", "-s", help="Stop if the agent exits with non-zero."),
230230
delay: float = typer.Option(0, "--delay", "-d", help="Seconds to wait between iterations."),
@@ -244,15 +244,15 @@ def run(
244244
args = agent.get("args", [])
245245

246246
try:
247-
ralph_file_path, resolved_ralph_name, prompt_text = resolve_ralph_source(
247+
ralph_file_path, resolved_ralph_name = resolve_ralph_source(
248248
prompt=prompt,
249249
toml_ralph=agent.get("ralph", "RALPH.md"),
250250
)
251251
except ValueError as e:
252252
rprint(f"[red]{e}[/red]")
253253
raise typer.Exit(1)
254254

255-
if not prompt_text and not Path(ralph_file_path).exists():
255+
if not Path(ralph_file_path).exists():
256256
rprint(f"[red]Prompt file '{ralph_file_path}' not found.[/red]")
257257
raise typer.Exit(1)
258258

@@ -263,7 +263,6 @@ def run(
263263
command=command,
264264
args=args,
265265
ralph_file=ralph_file_path,
266-
prompt_text=prompt_text,
267266
ralph_name=resolved_ralph_name,
268267
max_iterations=n,
269268
delay=delay,

src/ralphify/ralphs.py

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -91,43 +91,28 @@ def resolve_ralph_source(
9191
*,
9292
prompt: str | None,
9393
toml_ralph: str,
94-
) -> tuple[str, str | None, str | None]:
95-
"""Resolve the positional prompt argument into a file path, ralph name, or inline text.
94+
) -> tuple[str, str | None]:
95+
"""Resolve the positional argument into a ralph file path and optional name.
9696
97-
Returns ``(ralph_file_path, ralph_name, prompt_text)`` — exactly one of
98-
``ralph_name`` or ``prompt_text`` will be set, or both ``None`` when
99-
falling back to the toml/root prompt file.
97+
Returns ``(ralph_file_path, ralph_name)``.
10098
101-
Resolution order for the positional *prompt* argument:
99+
Resolution:
102100
103101
1. ``None`` → fall back to ``ralph.toml`` ``agent.ralph``
104-
2. Matches a named ralph in ``.ralphify/ralphs/`` → use that ralph
105-
3. Existing file path → use as prompt file
106-
4. Otherwise → treat as inline prompt text
102+
2. Otherwise → must match a named ralph in ``.ralphify/ralphs/``
107103
108-
Raises ``ValueError`` if a named ralph lookup fails (only when using
109-
the toml fallback with a name-like value).
104+
Raises ``ValueError`` if a named ralph lookup fails.
110105
"""
111106
if prompt is None:
112107
# Fall back to ralph.toml agent.ralph — could be a name or a path
113108
if is_ralph_name(toml_ralph):
114109
try:
115110
found = resolve_ralph_name(toml_ralph)
116-
return str(found.path / RALPH_MARKER), found.name, None
111+
return str(found.path / RALPH_MARKER), found.name
117112
except ValueError:
118-
return toml_ralph, None, None
119-
return toml_ralph, None, None
120-
121-
# Try as a named ralph first
122-
try:
123-
found = resolve_ralph_name(prompt)
124-
return str(found.path / RALPH_MARKER), found.name, None
125-
except ValueError:
126-
pass
127-
128-
# Try as a file path
129-
if Path(prompt).exists():
130-
return prompt, None, None
131-
132-
# Treat as inline prompt text
133-
return toml_ralph, None, prompt
113+
return toml_ralph, None
114+
return toml_ralph, None
115+
116+
# Must be a named ralph
117+
found = resolve_ralph_name(prompt)
118+
return str(found.path / RALPH_MARKER), found.name

tests/test_cli.py

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -269,46 +269,15 @@ def test_no_delay_with_single_iteration(self, mock_run, mock_sleep, tmp_path, mo
269269
mock_sleep.assert_not_called()
270270

271271

272-
class TestRunAdHocPrompt:
273-
@patch("ralphify._agent.subprocess.run", side_effect=_ok)
274-
def test_uses_provided_prompt_text(self, mock_run, tmp_path, monkeypatch):
272+
class TestRunRejectsInlinePrompt:
273+
def test_unknown_name_errors(self, tmp_path, monkeypatch):
274+
"""A value that doesn't match a named ralph produces an error."""
275275
monkeypatch.chdir(tmp_path)
276276
(tmp_path / CONFIG_FILENAME).write_text(RALPH_TOML_TEMPLATE)
277277

278278
result = runner.invoke(app, ["run", "do something", "-n", "1"])
279-
assert result.exit_code == 0
280-
assert mock_run.call_args.kwargs["input"] == "do something"
281-
282-
@patch("ralphify._agent.subprocess.run", side_effect=_ok)
283-
def test_skips_ralph_file_check(self, mock_run, tmp_path, monkeypatch):
284-
"""Works without RALPH.md when inline text is provided."""
285-
monkeypatch.chdir(tmp_path)
286-
(tmp_path / CONFIG_FILENAME).write_text(RALPH_TOML_TEMPLATE)
287-
# No RALPH.md created
288-
289-
result = runner.invoke(app, ["run", "ad-hoc prompt", "-n", "1"])
290-
assert result.exit_code == 0
291-
assert mock_run.call_count == 1
292-
293-
@patch("ralphify._agent.subprocess.run", side_effect=_ok)
294-
def test_file_path_used_as_prompt(self, mock_run, tmp_path, monkeypatch):
295-
"""An existing file path is used as the prompt file."""
296-
monkeypatch.chdir(tmp_path)
297-
(tmp_path / CONFIG_FILENAME).write_text(RALPH_TOML_TEMPLATE)
298-
(tmp_path / "alt.md").write_text("alternate prompt")
299-
300-
result = runner.invoke(app, ["run", "alt.md", "-n", "1"])
301-
assert result.exit_code == 0
302-
assert mock_run.call_args.kwargs["input"] == "alternate prompt"
303-
304-
def test_nonexistent_file_treated_as_inline(self, tmp_path, monkeypatch):
305-
"""A non-existent path-like string is treated as inline text."""
306-
monkeypatch.chdir(tmp_path)
307-
(tmp_path / CONFIG_FILENAME).write_text(RALPH_TOML_TEMPLATE)
308-
309-
# "nonexistent.md" doesn't exist as a file, and doesn't match a ralph name,
310-
# so it's treated as inline text
311-
result = runner.invoke(app, ["run", "nonexistent.md", "-n", "1"])
279+
assert result.exit_code == 1
280+
assert "not found" in result.output.lower()
312281

313282

314283
class TestRunLogging:
@@ -909,15 +878,14 @@ def test_run_with_ralph_name(self, mock_run, tmp_path, monkeypatch):
909878
assert result.exit_code == 0
910879
assert mock_run.call_args.kwargs["input"] == "Fix the docs."
911880

912-
@patch("ralphify._agent.subprocess.run", side_effect=_ok)
913-
def test_nonexistent_name_treated_as_inline_text(self, mock_run, tmp_path, monkeypatch):
914-
"""A value that doesn't match a ralph or file is treated as inline text."""
881+
def test_nonexistent_name_errors(self, tmp_path, monkeypatch):
882+
"""A value that doesn't match a named ralph produces an error."""
915883
monkeypatch.chdir(tmp_path)
916884
(tmp_path / CONFIG_FILENAME).write_text(RALPH_TOML_TEMPLATE)
917885

918886
result = runner.invoke(app, ["run", "nonexistent", "-n", "1"])
919-
assert result.exit_code == 0
920-
assert mock_run.call_args.kwargs["input"] == "nonexistent"
887+
assert result.exit_code == 1
888+
assert "not found" in result.output.lower()
921889

922890
@patch("ralphify._agent.subprocess.run", side_effect=_ok)
923891
def test_run_without_name_falls_back_to_toml(self, mock_run, tmp_path, monkeypatch):
@@ -941,11 +909,11 @@ def test_toml_ralph_as_name(self, mock_run, tmp_path, monkeypatch):
941909
assert result.exit_code == 0
942910
assert mock_run.call_args.kwargs["input"] == "Fix the docs."
943911

944-
@patch("ralphify._agent.subprocess.run", side_effect=_ok)
945-
def test_inline_prompt_used_when_not_ralph_or_file(self, mock_run, tmp_path, monkeypatch):
912+
def test_inline_text_rejected(self, tmp_path, monkeypatch):
913+
"""Inline text that isn't a ralph name produces an error."""
946914
monkeypatch.chdir(tmp_path)
947915
(tmp_path / CONFIG_FILENAME).write_text(RALPH_TOML_TEMPLATE)
948916

949917
result = runner.invoke(app, ["run", "inline text", "-n", "1"])
950-
assert result.exit_code == 0
951-
assert mock_run.call_args.kwargs["input"] == "inline text"
918+
assert result.exit_code == 1
919+
assert "not found" in result.output.lower()

0 commit comments

Comments
 (0)