From c32f37d2ef047a62883f1e0cd6484bbe85ab9386 Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Tue, 7 Jul 2026 01:11:42 +0500 Subject: [PATCH 1/2] fix(agents): resolve skill placeholders in Goose (yaml) command output CommandRegistrar.register_commands resolves {SCRIPT}/__AGENT__ and the $ARGUMENTS placeholder in the markdown and toml branches, but the yaml branch (Goose recipes) called render_yaml_command directly, skipping both. So extension/preset command bodies installed for Goose kept literal {SCRIPT}, __AGENT__, and repo-relative script paths in the generated .goose/recipes/*.yaml prompt. Mirror the markdown/toml branches: run resolve_skill_placeholders + _convert_argument_placeholder on the body before render_yaml_command. Co-Authored-By: Claude Fable 5 --- src/specify_cli/agents.py | 6 +++ tests/integrations/test_integration_goose.py | 40 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index 7864260a99..bdf09c081c 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -673,6 +673,12 @@ def register_commands( ) output = self.render_toml_command(frontmatter, body, source_id) elif agent_config["format"] == "yaml": + body = self.resolve_skill_placeholders( + agent_name, frontmatter, body, project_root + ) + body = self._convert_argument_placeholder( + body, "$ARGUMENTS", agent_config["args"] + ) output = self.render_yaml_command( frontmatter, body, source_id, cmd_name ) diff --git a/tests/integrations/test_integration_goose.py b/tests/integrations/test_integration_goose.py index 104b7188d0..76574ce4a4 100644 --- a/tests/integrations/test_integration_goose.py +++ b/tests/integrations/test_integration_goose.py @@ -36,3 +36,43 @@ def test_setup_declares_args_parameter_for_args_prompt(self, tmp_path): param.get("key") == "args" for param in data.get("parameters", []) ), f"{recipe_file} uses {{{{args}}}} but does not declare args" + + +class TestGooseCommandPlaceholderResolution: + """register_commands must resolve skill placeholders for the yaml branch. + + The yaml (Goose recipe) branch previously skipped + resolve_skill_placeholders / _convert_argument_placeholder that the + markdown and toml branches apply, so extension/preset command bodies + kept literal {SCRIPT} / __AGENT__ / repo-relative paths. + """ + + def test_register_commands_resolves_placeholders_in_recipe(self, tmp_path): + from specify_cli.agents import CommandRegistrar + + ext_dir = tmp_path / "extension" + cmd_dir = ext_dir / "commands" + cmd_dir.mkdir(parents=True) + cmd_file = cmd_dir / "example.md" + cmd_file.write_text( + "---\n" + "description: Placeholder command\n" + "scripts:\n" + " sh: scripts/bash/do.sh\n" + " ps: scripts/powershell/do.ps1\n" + "---\n\n" + "Run {SCRIPT} for agent __AGENT__ with $ARGUMENTS.\n", + encoding="utf-8", + ) + + registrar = CommandRegistrar() + commands = [{"name": "speckit.example", "file": "commands/example.md"}] + registrar.register_commands("goose", commands, "test-ext", ext_dir, tmp_path) + + recipe = tmp_path / ".goose" / "recipes" / "speckit.example.yaml" + assert recipe.exists(), "goose recipe should be generated" + content = recipe.read_text(encoding="utf-8") + # Placeholders must be resolved, not emitted verbatim. + assert "{SCRIPT}" not in content + assert "__AGENT__" not in content + assert "$ARGUMENTS" not in content From 1c46324c8bb6ecde7d379dbbec212032150d25ff Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Tue, 7 Jul 2026 22:12:03 +0500 Subject: [PATCH 2/2] test(goose): assert positive placeholder replacements in recipe prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review: parse the generated recipe with yaml.safe_load and assert the prompt contains the resolved values (.specify/scripts/, 'agent goose', {{args}}), not merely that the literal tokens are absent — a wrong output that happens to omit the exact strings would otherwise pass. Co-Authored-By: Claude Fable 5 --- tests/integrations/test_integration_goose.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/integrations/test_integration_goose.py b/tests/integrations/test_integration_goose.py index 76574ce4a4..300b056c47 100644 --- a/tests/integrations/test_integration_goose.py +++ b/tests/integrations/test_integration_goose.py @@ -71,8 +71,15 @@ def test_register_commands_resolves_placeholders_in_recipe(self, tmp_path): recipe = tmp_path / ".goose" / "recipes" / "speckit.example.yaml" assert recipe.exists(), "goose recipe should be generated" - content = recipe.read_text(encoding="utf-8") - # Placeholders must be resolved, not emitted verbatim. - assert "{SCRIPT}" not in content - assert "__AGENT__" not in content - assert "$ARGUMENTS" not in content + # Parse the recipe and assert the prompt actually got the correct + # replacements — not merely that the literal tokens are absent (which + # a wrong-but-token-free output could also satisfy). + data = yaml.safe_load(recipe.read_text(encoding="utf-8")) + prompt = data["prompt"] + assert ".specify/scripts/" in prompt # {SCRIPT} -> resolved script path + assert "agent goose" in prompt # __AGENT__ -> agent name + assert "{{args}}" in prompt # $ARGUMENTS -> goose args token + # And the raw placeholders must not survive. + assert "{SCRIPT}" not in prompt + assert "__AGENT__" not in prompt + assert "$ARGUMENTS" not in prompt