Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions generator/src/tend/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,20 @@ def _escape_braces(prompt: str, placeholder: str) -> tuple[str, bool]:
Returns (escaped_prompt, needs_format). In the escaped prompt, {placeholder}
is replaced with {0} for use with GitHub Actions format(), and all other
braces are doubled to prevent format() from interpreting them.

A prompt with no {placeholder} is returned untouched. Doubling is only
correct on the way into `format()`, which collapses each pair back to one
brace; without the placeholder the caller emits a bare string literal
instead, and GitHub Actions does not collapse braces there — the pairs
would reach the agent verbatim.
"""
sentinel = "\x00PLACEHOLDER\x00"
text = prompt.replace(f"{{{placeholder}}}", sentinel)
if sentinel not in text:
return prompt, False
# Double all remaining braces so format() treats them as literals
text = text.replace("{", "{{").replace("}", "}}")
has_placeholder = sentinel in text
text = text.replace(sentinel, "{0}")
return text, has_placeholder
return text.replace(sentinel, "{0}"), True


def _effective_cfg(cfg: Config, wf: WorkflowConfig) -> Config:
Expand Down
13 changes: 9 additions & 4 deletions generator/tests/test_config_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,13 @@ def test_prompt_with_zero_placeholder(tmp_path: Path) -> None:


def test_prompt_with_numbered_placeholders(tmp_path: Path) -> None:
"""Prompt with {1}, {2} — escaped to prevent format() runtime errors."""
"""Prompt with {1}, {2} and no {pr_number} — emitted verbatim, not escaped.

Escaping guards `format()`, which collapses each doubled pair back to one
brace. With no {pr_number} there is nothing to interpolate, so the prompt
is emitted as a bare GHA string literal instead — nothing collapses the
pairs there, and doubling would ship `{{1}}` to the agent.
"""
path = _write_config(
tmp_path,
dedent("""\
Expand All @@ -312,9 +318,8 @@ def test_prompt_with_numbered_placeholders(tmp_path: Path) -> None:
cfg = Config.load(path)
workflows = {wf.filename: wf for wf in generate_all(cfg)}
review = workflows["tend-review.yaml"]
# {1} and {2} are escaped to {{1}} and {{2}} — literals in GHA expressions
assert "{{1}}" in review.content
assert "{{2}}" in review.content
assert "format(" not in review.content
assert "'Fix issue {1} and {2}'" in review.content


# ---------------------------------------------------------------------------
Expand Down
49 changes: 49 additions & 0 deletions generator/tests/test_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from tend.workflows import (
_deep_merge,
GENERATORS,
GeneratedWorkflow,
generate_all,
generate_install_test,
generate_mention,
Expand Down Expand Up @@ -395,6 +396,54 @@ def test_custom_prompt(tmp_path: Path) -> None:
assert "Custom triage:" in triage.content


def _review_prompt(review: GeneratedWorkflow) -> str:
"""The `prompt:` input the review job hands the harness action."""
steps = yaml.safe_load(review.content)["jobs"]["review"]["steps"]
step = next(
s for s in steps if s.get("uses", "").startswith("max-sixty/tend/claude@")
)
return step["with"]["prompt"]


def test_review_prompt_without_placeholder_keeps_literal_braces(
tmp_path: Path,
) -> None:
"""A review prompt with braces but no `{pr_number}` reaches the agent verbatim.

The review prompt is the only one emitted inside a GHA expression. With the
placeholder it goes through `format()`, which needs every other brace
doubled; without it, it is a bare string literal that GHA never collapses,
so doubling there would ship `{{...}}` to the agent.
"""
extra = dedent("""\
workflows:
review:
prompt: "Review this PR. Skip files matching {generated}."
""")
cfg = Config.load(_minimal_config(tmp_path, extra))
workflows = {wf.filename: wf for wf in generate_all(cfg)}
prompt = _review_prompt(workflows["tend-review.yaml"])
assert "{generated}" in prompt
assert "{{generated}}" not in prompt
assert "format(" not in prompt


def test_review_prompt_with_placeholder_escapes_other_braces(tmp_path: Path) -> None:
"""With `{pr_number}` present the prompt goes through `format()`, so the
placeholder becomes `{0}` and every other brace is doubled for it."""
extra = dedent("""\
workflows:
review:
prompt: "Review PR {pr_number}. Skip files matching {generated}."
""")
cfg = Config.load(_minimal_config(tmp_path, extra))
workflows = {wf.filename: wf for wf in generate_all(cfg)}
prompt = _review_prompt(workflows["tend-review.yaml"])
assert "format(" in prompt
assert "{0}" in prompt
assert "{{generated}}" in prompt


def test_watched_workflows(tmp_path: Path) -> None:
extra = dedent("""\
workflows:
Expand Down
Loading