Skip to content
Merged
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"
3 changes: 1 addition & 2 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1115,7 +1115,7 @@ def init(
console.print(f"[cyan]--force supplied: merging into existing directory '[cyan]{project_name}[/cyan]'[/cyan]")
else:
error_panel = Panel(
f"Directory '[cyan]{project_name}[/cyan]' already exists\n"
f"Directory already exists: '[cyan]{project_name}[/cyan]'\n"
"Please choose a different project name or remove the existing directory.\n"
"Use [bold]--force[/bold] to merge into the existing directory.",
title="[red]Directory Conflict[/red]",
Expand Down Expand Up @@ -1371,7 +1371,6 @@ def init(
"branch_numbering": branch_numbering or "sequential",
"context_file": resolved_integration.context_file,
"here": here,
"preset": preset,
"script": selected_script,
"speckit_version": get_speckit_version(),
}
Expand Down
17 changes: 14 additions & 3 deletions src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,15 @@ 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
body, core_frontmatter = _substitute_core_template(body, cmd_name, project_root, self)
frontmatter = dict(frontmatter)
Comment thread
mnriem marked this conversation as resolved.
for key in ("scripts", "agent_scripts"):
if key not in frontmatter and key in core_frontmatter:
frontmatter[key] = core_frontmatter[key]
frontmatter.pop("strategy", None)

frontmatter = self._adjust_script_paths(frontmatter)

for key in agent_config.get("strip_frontmatter_keys", []):
Expand Down Expand Up @@ -472,10 +481,12 @@ def register_commands(
project_root,
)
elif agent_config["format"] == "markdown":
output = self.render_markdown_command(
frontmatter, body, source_id, context_note
)
body = self.resolve_skill_placeholders(agent_name, frontmatter, body, project_root)
body = self._convert_argument_placeholder(body, "$ARGUMENTS", agent_config["args"])
output = self.render_markdown_command(frontmatter, body, source_id, context_note)
elif agent_config["format"] == "toml":
body = self.resolve_skill_placeholders(agent_name, frontmatter, body, project_root)
body = self._convert_argument_placeholder(body, "$ARGUMENTS", agent_config["args"])
output = self.render_toml_command(frontmatter, body, source_id)
elif agent_config["format"] == "yaml":
output = self.render_yaml_command(
Expand Down
7 changes: 6 additions & 1 deletion src/specify_cli/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,16 @@ def _load_yaml(self, path: Path) -> dict:
"""Load YAML file safely."""
try:
with open(path, 'r') as f:
return yaml.safe_load(f) or {}
data = yaml.safe_load(f)
except yaml.YAMLError as e:
raise ValidationError(f"Invalid YAML in {path}: {e}")
except FileNotFoundError:
raise ValidationError(f"Manifest not found: {path}")
if not isinstance(data, dict):
raise ValidationError(
f"Manifest must be a YAML mapping, got {type(data).__name__}: {path}"
)
return data

def _validate(self):
"""Validate manifest structure and required fields."""
Expand Down
Loading
Loading