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
2 changes: 1 addition & 1 deletion presets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,5 @@ The following enhancements are under consideration for future releases:
| **command** | ✓ (default) | ✓ | ✓ | ✓ |
| **script** | ✓ (default) | — | — | ✓ |

For artifacts and commands (which are LLM directives), `wrap` would inject preset content before and after the core template using a `{CORE_TEMPLATE}` placeholder. For scripts, `wrap` would run custom logic before/after the core script via a `$CORE_SCRIPT` variable.
For artifacts and commands (which are LLM directives), `wrap` injects preset content before and after the core template using a `{CORE_TEMPLATE}` placeholder (implemented). For scripts, `wrap` would run custom logic before/after the core script via a `$CORE_SCRIPT` variable (not yet implemented).
- **Script overrides** — Enable presets to provide alternative versions of core scripts (e.g. `create-new-feature.sh`) for workflow customization. A `strategy: "wrap"` option could allow presets to run custom logic before/after the core script without fully replacing it.
14 changes: 14 additions & 0 deletions presets/self-test/commands/speckit.wrap-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
description: "Self-test wrap command — pre/post around core"
strategy: wrap
---

## Preset Pre-Logic

preset:self-test wrap-pre

{CORE_TEMPLATE}

## Preset Post-Logic

preset:self-test wrap-post
5 changes: 5 additions & 0 deletions presets/self-test/preset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ provides:
description: "Self-test override of the specify command"
replaces: "speckit.specify"

- type: "command"
name: "speckit.wrap-test"
file: "commands/speckit.wrap-test.md"
description: "Self-test wrap strategy command"

tags:
- "testing"
- "self-test"
7 changes: 7 additions & 0 deletions src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,13 @@ def register_commands(
content = source_file.read_text(encoding="utf-8")
frontmatter, body = self.parse_frontmatter(content)

if frontmatter.get("strategy") == "wrap":
from .presets import _substitute_core_template
short_name = cmd_name
if short_name.startswith("speckit."):
short_name = short_name[len("speckit."):]
body = _substitute_core_template(body, short_name, project_root, self)

frontmatter = self._adjust_script_paths(frontmatter)

for key in agent_config.get("strip_frontmatter_keys", []):
Expand Down
40 changes: 40 additions & 0 deletions src/specify_cli/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,43 @@
from .extensions import ExtensionRegistry, normalize_priority


def _substitute_core_template(
body: str,
short_name: str,
project_root: "Path",
registrar: "CommandRegistrar",
) -> str:
"""Substitute {CORE_TEMPLATE} with the body of the installed core command template.

Args:
body: Preset command body (may contain {CORE_TEMPLATE} placeholder).
short_name: Short command name (e.g. "specify" from "speckit.specify").
project_root: Project root path.
registrar: CommandRegistrar instance for parse_frontmatter.

Returns:
Body with {CORE_TEMPLATE} replaced by core template body, or body unchanged
if the placeholder is absent or the core template file does not exist.
"""
if "{CORE_TEMPLATE}" not in body:
return body

from specify_cli import _locate_core_pack
core_pack = _locate_core_pack()
if core_pack is not None:
core_file = core_pack / "commands" / f"{short_name}.md"
else:
# Source / editable install: look relative to the package root
repo_root = Path(__file__).parent.parent.parent
core_file = repo_root / "templates" / "commands" / f"{short_name}.md"

if not core_file.exists():
return body

_, core_body = registrar.parse_frontmatter(core_file.read_text(encoding="utf-8"))
return body.replace("{CORE_TEMPLATE}", core_body)


@dataclass
class PresetCatalogEntry:
"""Represents a single entry in the preset catalog stack."""
Expand Down Expand Up @@ -759,6 +796,9 @@ def _register_skills(
content = source_file.read_text(encoding="utf-8")
frontmatter, body = registrar.parse_frontmatter(content)

if frontmatter.get("strategy") == "wrap":
body = _substitute_core_template(body, short_name, self.project_root, registrar)

original_desc = frontmatter.get("description", "")
enhanced_desc = SKILL_DESCRIPTIONS.get(
short_name,
Expand Down
136 changes: 134 additions & 2 deletions tests/test_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1667,7 +1667,7 @@ def test_self_test_manifest_valid(self):
assert manifest.id == "self-test"
assert manifest.name == "Self-Test Preset"
assert manifest.version == "1.0.0"
assert len(manifest.templates) == 7 # 6 templates + 1 command
assert len(manifest.templates) == 8 # 6 templates + 2 commands

def test_self_test_provides_all_core_templates(self):
"""Verify the self-test preset provides an override for every core template."""
Expand Down Expand Up @@ -3043,4 +3043,136 @@ def test_bundled_preset_missing_locally_cli_error(self, project_dir):
assert result.exit_code == 1
output = strip_ansi(result.output).lower()
assert "bundled" in output, result.output
assert "reinstall" in output, result.output


class TestWrapStrategy:
"""Tests for strategy: wrap preset command substitution."""

def test_substitute_core_template_replaces_placeholder(self, project_dir):
"""Core template body replaces {CORE_TEMPLATE} in preset command body."""
from specify_cli.presets import _substitute_core_template
from specify_cli.agents import CommandRegistrar

# Set up a core command template
core_dir = project_dir / ".specify" / "templates" / "commands"
core_dir.mkdir(parents=True, exist_ok=True)
(core_dir / "specify.md").write_text(
"---\ndescription: core\n---\n\n# Core Specify\n\nDo the thing.\n"
)

registrar = CommandRegistrar()
body = "## Pre-Logic\n\nBefore stuff.\n\n{CORE_TEMPLATE}\n\n## Post-Logic\n\nAfter stuff.\n"
result = _substitute_core_template(body, "specify", project_dir, registrar)

assert "{CORE_TEMPLATE}" not in result
assert "# Core Specify" in result
assert "## Pre-Logic" in result
assert "## Post-Logic" in result

def test_substitute_core_template_no_op_when_placeholder_absent(self, project_dir):
"""Returns body unchanged when {CORE_TEMPLATE} is not present."""
from specify_cli.presets import _substitute_core_template
from specify_cli.agents import CommandRegistrar

core_dir = project_dir / ".specify" / "templates" / "commands"
core_dir.mkdir(parents=True, exist_ok=True)
(core_dir / "specify.md").write_text("---\ndescription: core\n---\n\nCore body.\n")

registrar = CommandRegistrar()
body = "## No placeholder here.\n"
result = _substitute_core_template(body, "specify", project_dir, registrar)
assert result == body

def test_substitute_core_template_no_op_when_core_missing(self, project_dir):
"""Returns body unchanged when core template file does not exist."""
from specify_cli.presets import _substitute_core_template
from specify_cli.agents import CommandRegistrar

registrar = CommandRegistrar()
body = "Pre.\n\n{CORE_TEMPLATE}\n\nPost.\n"
result = _substitute_core_template(body, "nonexistent", project_dir, registrar)
assert result == body
assert "{CORE_TEMPLATE}" in result

def test_register_commands_substitutes_core_template_for_wrap_strategy(self, project_dir):
"""register_commands substitutes {CORE_TEMPLATE} when strategy: wrap."""
from specify_cli.agents import CommandRegistrar

# Set up core command template
core_dir = project_dir / ".specify" / "templates" / "commands"
core_dir.mkdir(parents=True, exist_ok=True)
(core_dir / "specify.md").write_text(
"---\ndescription: core\n---\n\n# Core Specify\n\nCore body here.\n"
)

# Create a preset command dir with a wrap-strategy command
cmd_dir = project_dir / "preset" / "commands"
cmd_dir.mkdir(parents=True, exist_ok=True)
(cmd_dir / "speckit.specify.md").write_text(
"---\ndescription: wrap test\nstrategy: wrap\n---\n\n"
"## Pre\n\n{CORE_TEMPLATE}\n\n## Post\n"
)

commands = [{"name": "speckit.specify", "file": "commands/speckit.specify.md"}]
registrar = CommandRegistrar()

# Use a generic agent that writes markdown to commands/
agent_dir = project_dir / ".claude" / "commands"
agent_dir.mkdir(parents=True, exist_ok=True)

# Patch AGENT_CONFIGS to use a simple markdown agent pointing at our dir
import copy
original = copy.deepcopy(registrar.AGENT_CONFIGS)
registrar.AGENT_CONFIGS["test-agent"] = {
"dir": str(agent_dir.relative_to(project_dir)),
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
"strip_frontmatter_keys": [],
}
try:
registrar.register_commands(
"test-agent", commands, "test-preset",
project_dir / "preset", project_dir
)
finally:
registrar.AGENT_CONFIGS = original

written = (agent_dir / "speckit.specify.md").read_text()
assert "{CORE_TEMPLATE}" not in written
assert "# Core Specify" in written
assert "## Pre" in written
assert "## Post" in written

def test_end_to_end_wrap_via_self_test_preset(self, project_dir):
"""Installing self-test preset with a wrap command substitutes {CORE_TEMPLATE}."""
from specify_cli.presets import PresetManager

# Install a core template that wrap-test will wrap around
core_dir = project_dir / ".specify" / "templates" / "commands"
core_dir.mkdir(parents=True, exist_ok=True)
(core_dir / "wrap-test.md").write_text(
"---\ndescription: core wrap-test\n---\n\n# Core Wrap-Test Body\n"
)

# Set up skills dir (simulating --ai claude)
skills_dir = project_dir / ".claude" / "skills"
skills_dir.mkdir(parents=True, exist_ok=True)
skill_subdir = skills_dir / "speckit-wrap-test"
skill_subdir.mkdir()
(skill_subdir / "SKILL.md").write_text("---\nname: speckit-wrap-test\n---\n\nold content\n")

# Write init-options so _register_skills finds the claude skills dir
import json
(project_dir / ".specify" / "init-options.json").write_text(
json.dumps({"ai": "claude", "ai_skills": True})
)

manager = PresetManager(project_dir)
manager.install_from_directory(SELF_TEST_PRESET_DIR, "0.1.5")

written = (skill_subdir / "SKILL.md").read_text()
assert "{CORE_TEMPLATE}" not in written
assert "# Core Wrap-Test Body" in written
assert "preset:self-test wrap-pre" in written
assert "preset:self-test wrap-post" in written