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: 11 additions & 1 deletion src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,17 @@ def render_yaml_command(
description = frontmatter.get("description", "")
if not isinstance(description, str):
description = str(description) if description is not None else ""
return YamlIntegration._render_yaml(title, description, body, source_id)
params = None
if "{{args}}" in body:
params = [
{
"key": "args",
"input_type": "string",
"requirement": "user_prompt",
"description": "Arguments to pass to the command",
}
]
return YamlIntegration._render_yaml(title, description, body, source_id, parameters=params)

def render_skill_command(
self,
Expand Down
26 changes: 24 additions & 2 deletions src/specify_cli/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1202,7 +1202,13 @@ def _human_title(identifier: str) -> str:
return text.replace(".", " ").replace("-", " ").replace("_", " ").title()

@staticmethod
def _render_yaml(title: str, description: str, body: str, source_id: str) -> str:
def _render_yaml(
title: str,
description: str,
body: str,
source_id: str,
parameters: list[dict[str, Any]] | None = None,
) -> str:
"""Render a YAML recipe file from title, description, and body.

Produces a Goose-compatible recipe with a literal block scalar
Expand All @@ -1220,6 +1226,9 @@ def _render_yaml(title: str, description: str, body: str, source_id: str) -> str
"activities": ["Spec-Driven Development"],
}

if parameters:
header["parameters"] = parameters

header_yaml = yaml.safe_dump(
header,
sort_keys=False,
Expand Down Expand Up @@ -1286,8 +1295,21 @@ def setup(
context_file=self.context_file or "",
)
_, body = self._split_frontmatter(processed)
# Build parameter definitions for template variables used in the body
params = None
if "{{args}}" in body:
params = [
{
"key": "args",
"input_type": "string",
"requirement": "user_prompt",
"description": "Arguments to pass to the command",
}
]

yaml_content = self._render_yaml(
title, description, body, f"templates/commands/{src_file.name}"
title, description, body, f"templates/commands/{src_file.name}",
parameters=params,
Comment on lines +1298 to +1312
Comment on lines +1298 to +1312
)
dst_name = self.command_filename(src_file.stem)
dst_file = self.write_file_and_record(
Expand Down
22 changes: 22 additions & 0 deletions tests/integrations/test_integration_base_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,28 @@ def test_yaml_uses_correct_arg_placeholder(self, tmp_path):
"YAML recipe still contains $ARGUMENTS instead of {{args}}"
)


def test_yaml_has_parameters_when_args_placeholder(self, tmp_path):
"""YAML recipes with {{args}} must include a parameters definition."""
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
created = i.setup(tmp_path, m)
cmd_files = [f for f in created if "scripts" not in f.parts]
assert len(cmd_files) > 0
for f in cmd_files:
content = f.read_text(encoding="utf-8")
if "{{args}}" in content:
lines = content.split("\n")
yaml_lines = [l for l in lines if not l.startswith("# Source:")]
parsed = yaml.safe_load("\n".join(yaml_lines))
assert "parameters" in parsed, (
f"{f.name} uses {{{{args}}}} but has no parameters definition"
)
params = parsed["parameters"]
assert any(p.get("key") == "args" for p in params), (
f"{f.name} parameters missing 'args' key"
)

def test_yaml_is_valid(self, tmp_path):
"""Every generated YAML file must parse without errors."""
i = get_integration(self.KEY)
Expand Down