diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index 726b0fd2a6..12af8f370c 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -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, diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index c46340ddff..a393f3046f 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -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 @@ -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, @@ -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, ) dst_name = self.command_filename(src_file.stem) dst_file = self.write_file_and_record( diff --git a/tests/integrations/test_integration_base_yaml.py b/tests/integrations/test_integration_base_yaml.py index 956c7a796f..416351ebe5 100644 --- a/tests/integrations/test_integration_base_yaml.py +++ b/tests/integrations/test_integration_base_yaml.py @@ -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)