Skip to content

Commit f19fec9

Browse files
Fix preset-wrap-drops-argument-hint: inherit argument-hint from core
Apply the remediation from the bug assessment on issue #3991. Extend the inheritance allowlist in _register_skills and _compose_layers to include 'argument-hint', so wrap-strategy presets that omit this key will inherit it from the core template rather than silently dropping it and risking its value being leaked into description. Refs #3991 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 71125fc commit f19fec9

2 files changed

Lines changed: 78 additions & 2 deletions

File tree

src/specify_cli/presets/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2743,7 +2743,7 @@ def _register_skills(
27432743
if frontmatter.get("strategy") == "wrap":
27442744
body, core_frontmatter = _substitute_core_template(body, cmd_name, self.project_root, registrar)
27452745
frontmatter = dict(frontmatter)
2746-
for key in ("scripts", "agent_scripts"):
2746+
for key in ("scripts", "agent_scripts", "argument-hint"):
27472747
if key not in frontmatter and key in core_frontmatter:
27482748
frontmatter[key] = core_frontmatter[key]
27492749

@@ -5725,7 +5725,7 @@ def _parse_fm_yaml(fm_block: str) -> dict:
57255725
# Inherit scripts/agent_scripts from base frontmatter if missing
57265726
if base_frontmatter_text and base_frontmatter_text != top_frontmatter_text:
57275727
base_fm = _parse_fm_yaml(base_frontmatter_text)
5728-
for key in ("scripts", "agent_scripts"):
5728+
for key in ("scripts", "agent_scripts", "argument-hint"):
57295729
if key not in top_fm and key in base_fm:
57305730
top_fm[key] = base_fm[key]
57315731

tests/test_presets.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4727,6 +4727,82 @@ def test_argument_hint_not_added_for_non_claude_preset_command(self, project_dir
47274727
parsed = yaml.safe_load(skill_file.read_text(encoding="utf-8").split("---", 2)[1])
47284728
assert "argument-hint" not in parsed
47294729

4730+
def test_wrap_preset_inherits_argument_hint_from_core(self, project_dir, temp_dir):
4731+
"""A wrap-strategy preset that omits argument-hint must inherit it from the core template.
4732+
4733+
Regression for issue #3991: the wrap-composition path in _register_skills
4734+
previously inherited only scripts/agent_scripts from core_frontmatter,
4735+
silently discarding argument-hint and leaking its value into description.
4736+
"""
4737+
core_arg_hint = "Describe the feature you want to specify"
4738+
preset_description = "Wrapped speckit.specify — extra project context added"
4739+
self._write_init_options(project_dir, ai="claude")
4740+
skills_dir = project_dir / ".claude" / "skills"
4741+
self._create_skill(skills_dir, "speckit-specify")
4742+
4743+
# Place a core template that declares argument-hint
4744+
core_cmds = project_dir / ".specify" / "templates" / "commands"
4745+
core_cmds.mkdir(parents=True, exist_ok=True)
4746+
(core_cmds / "specify.md").write_text(
4747+
"---\n"
4748+
"description: Core specify description.\n"
4749+
f'argument-hint: "{core_arg_hint}"\n'
4750+
"---\n\n"
4751+
"Core specify body.\n",
4752+
encoding="utf-8",
4753+
)
4754+
4755+
# Wrap preset: only declares description (no argument-hint)
4756+
preset_dir = temp_dir / "wrap-hint-preset"
4757+
preset_dir.mkdir()
4758+
(preset_dir / "commands").mkdir()
4759+
(preset_dir / "commands" / "speckit.specify.md").write_text(
4760+
"---\n"
4761+
f'description: "{preset_description}"\n'
4762+
"strategy: wrap\n"
4763+
"---\n\n"
4764+
"{CORE_TEMPLATE}\n",
4765+
encoding="utf-8",
4766+
)
4767+
manifest_data = {
4768+
"schema_version": "1.0",
4769+
"preset": {
4770+
"id": "wrap-hint-preset",
4771+
"name": "Wrap Hint Preset",
4772+
"version": "1.0.0",
4773+
"description": "Test wrap hint inheritance",
4774+
},
4775+
"requires": {"speckit_version": ">=0.1.0"},
4776+
"provides": {
4777+
"templates": [
4778+
{
4779+
"type": "command",
4780+
"name": "speckit.specify",
4781+
"file": "commands/speckit.specify.md",
4782+
"strategy": "wrap",
4783+
}
4784+
]
4785+
},
4786+
}
4787+
import yaml as _yaml
4788+
with open(preset_dir / "preset.yml", "w") as f:
4789+
_yaml.dump(manifest_data, f)
4790+
4791+
manager = PresetManager(project_dir)
4792+
manager.install_from_directory(preset_dir, "1.0.0")
4793+
4794+
skill_file = skills_dir / "speckit-specify" / "SKILL.md"
4795+
assert skill_file.exists()
4796+
parsed = yaml.safe_load(skill_file.read_text(encoding="utf-8").split("---", 2)[1])
4797+
# argument-hint must be inherited from core, not dropped
4798+
assert parsed.get("argument-hint") == core_arg_hint, (
4799+
f"argument-hint was not inherited from core; parsed={parsed}"
4800+
)
4801+
# description must be exactly the preset's declared value, not concatenated
4802+
assert parsed["description"] == preset_description, (
4803+
f"description was corrupted; parsed={parsed}"
4804+
)
4805+
47304806
def test_register_skills_resolves_command_refs(self, project_dir, temp_dir):
47314807
"""Preset skill overrides must resolve __SPECKIT_COMMAND_*__ tokens (issue #2717).
47324808

0 commit comments

Comments
 (0)