mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
* fix(forge): use hyphen notation for command refs in Forge integration - Add invoke_separator = "-" class attribute to ForgeIntegration so effective_invoke_separator() returns "-" for shared-template installs - Add "invoke_separator": "-" to ForgeIntegration.registrar_config so agents.py CommandRegistrar can resolve refs with the correct separator - Pass invoke_separator to process_template() in ForgeIntegration.setup() so all .forge/commands/*.md bodies use /speckit-foo notation - Replace literal /speckit.specify with __SPECKIT_COMMAND_SPECIFY__ in extensions/git/commands/speckit.git.feature.md so every agent resolves the reference through its own separator - Apply resolve_command_refs re.sub in agents.py register_commands() after argument-placeholder substitution so extension commands registered for Forge get /speckit-foo refs; all other agents continue to get /speckit.foo Fixes ZSH compatibility: dot-notation command invocations (/speckit.specify) are misinterpreted by ZSH as file-path operations; hyphen notation (/speckit-specify) works correctly in all shells. * fix(agents): propagate invoke_separator from integration class into AGENT_CONFIGS Skills-based agents (claude, codex, kimi, …) inherit invoke_separator="-" from SkillsIntegration but do not repeat it in their registrar_config dicts. _build_agent_configs() was copying registrar_config verbatim, so register_commands() fell back to "." when resolving __SPECKIT_COMMAND_*__ tokens for those agents — emitting /speckit.specify instead of the correct /speckit-specify for extension commands like speckit.git.feature. Fix: after copying registrar_config, inject invoke_separator from the integration's class attribute when it is not already declared explicitly. This makes the integration class the single source of truth for all agents, without requiring each SkillsIntegration subclass to duplicate the field. Also replace the inline re.sub in register_commands() with a call to IntegrationBase.resolve_command_refs() (deferred import to avoid the existing circular dependency) so token-resolution logic is not duplicated. Adds two tests in test_agent_config_consistency.py: - test_skills_agents_have_hyphen_invoke_separator_in_agent_configs: asserts every /SKILL.md agent has invoke_separator="-" in AGENT_CONFIGS. - test_skills_agent_command_token_resolves_with_hyphen: end-to-end check via CommandRegistrar that the git extension's speckit.git.feature command is installed for Claude with /speckit-specify (not /speckit.specify). Addresses review comment on PR #2462.
482 lines
21 KiB
Python
482 lines
21 KiB
Python
"""Tests for ForgeIntegration."""
|
|
|
|
from specify_cli.integrations import get_integration
|
|
from specify_cli.integrations.manifest import IntegrationManifest
|
|
from specify_cli.integrations.forge import format_forge_command_name
|
|
|
|
|
|
class TestForgeCommandNameFormatter:
|
|
"""Test the centralized Forge command name formatter."""
|
|
|
|
def test_simple_name_without_prefix(self):
|
|
"""Test formatting a simple name without 'speckit.' prefix."""
|
|
assert format_forge_command_name("plan") == "speckit-plan"
|
|
assert format_forge_command_name("tasks") == "speckit-tasks"
|
|
assert format_forge_command_name("specify") == "speckit-specify"
|
|
|
|
def test_name_with_speckit_prefix(self):
|
|
"""Test formatting a name that already has 'speckit.' prefix."""
|
|
assert format_forge_command_name("speckit.plan") == "speckit-plan"
|
|
assert format_forge_command_name("speckit.tasks") == "speckit-tasks"
|
|
|
|
def test_extension_command_name(self):
|
|
"""Test formatting extension command names with dots."""
|
|
assert format_forge_command_name("speckit.my-extension.example") == "speckit-my-extension-example"
|
|
assert format_forge_command_name("my-extension.example") == "speckit-my-extension-example"
|
|
|
|
def test_complex_nested_name(self):
|
|
"""Test formatting deeply nested command names."""
|
|
assert format_forge_command_name("speckit.jira.sync-status") == "speckit-jira-sync-status"
|
|
assert format_forge_command_name("speckit.foo.bar.baz") == "speckit-foo-bar-baz"
|
|
|
|
def test_name_with_hyphens_preserved(self):
|
|
"""Test that existing hyphens are preserved."""
|
|
assert format_forge_command_name("my-extension") == "speckit-my-extension"
|
|
assert format_forge_command_name("speckit.my-ext.test-cmd") == "speckit-my-ext-test-cmd"
|
|
|
|
def test_alias_formatting(self):
|
|
"""Test formatting alias names."""
|
|
assert format_forge_command_name("speckit.my-extension.example-short") == "speckit-my-extension-example-short"
|
|
|
|
def test_idempotent_already_hyphenated(self):
|
|
"""Test that already-hyphenated names are returned unchanged (idempotent)."""
|
|
assert format_forge_command_name("speckit-plan") == "speckit-plan"
|
|
assert format_forge_command_name("speckit-my-extension-example") == "speckit-my-extension-example"
|
|
assert format_forge_command_name("speckit-jira-sync-status") == "speckit-jira-sync-status"
|
|
|
|
|
|
class TestForgeIntegration:
|
|
def test_forge_key_and_config(self):
|
|
forge = get_integration("forge")
|
|
assert forge is not None
|
|
assert forge.key == "forge"
|
|
assert forge.config["folder"] == ".forge/"
|
|
assert forge.config["commands_subdir"] == "commands"
|
|
assert forge.config["requires_cli"] is True
|
|
assert forge.registrar_config["args"] == "{{parameters}}"
|
|
assert forge.registrar_config["extension"] == ".md"
|
|
assert forge.context_file == "AGENTS.md"
|
|
|
|
def test_command_filename_md(self):
|
|
forge = get_integration("forge")
|
|
assert forge.command_filename("plan") == "speckit.plan.md"
|
|
|
|
def test_setup_creates_md_files(self, tmp_path):
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
forge = ForgeIntegration()
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
created = forge.setup(tmp_path, m)
|
|
assert len(created) > 0
|
|
# Separate command files from scripts
|
|
command_files = [f for f in created if f.parent == tmp_path / ".forge" / "commands"]
|
|
assert len(command_files) > 0
|
|
for f in command_files:
|
|
assert f.name.endswith(".md")
|
|
|
|
def test_setup_upserts_context_section(self, tmp_path):
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
forge = ForgeIntegration()
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
forge.setup(tmp_path, m)
|
|
ctx_path = tmp_path / forge.context_file
|
|
assert ctx_path.exists()
|
|
content = ctx_path.read_text(encoding="utf-8")
|
|
assert "<!-- SPECKIT START -->" in content
|
|
assert "<!-- SPECKIT END -->" in content
|
|
|
|
def test_all_created_files_tracked_in_manifest(self, tmp_path):
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
forge = ForgeIntegration()
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
created = forge.setup(tmp_path, m)
|
|
for f in created:
|
|
rel = f.resolve().relative_to(tmp_path.resolve()).as_posix()
|
|
assert rel in m.files, f"Created file {rel} not tracked in manifest"
|
|
|
|
def test_install_uninstall_roundtrip(self, tmp_path):
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
forge = ForgeIntegration()
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
created = forge.install(tmp_path, m)
|
|
assert len(created) > 0
|
|
m.save()
|
|
for f in created:
|
|
assert f.exists()
|
|
removed, skipped = forge.uninstall(tmp_path, m)
|
|
assert len(removed) == len(created)
|
|
assert skipped == []
|
|
|
|
def test_modified_file_survives_uninstall(self, tmp_path):
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
forge = ForgeIntegration()
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
created = forge.install(tmp_path, m)
|
|
m.save()
|
|
# Modify a command file (not a script)
|
|
command_files = [f for f in created if f.parent == tmp_path / ".forge" / "commands"]
|
|
modified_file = command_files[0]
|
|
modified_file.write_text("user modified this", encoding="utf-8")
|
|
removed, skipped = forge.uninstall(tmp_path, m)
|
|
assert modified_file.exists()
|
|
assert modified_file in skipped
|
|
|
|
def test_directory_structure(self, tmp_path):
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
forge = ForgeIntegration()
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
forge.setup(tmp_path, m)
|
|
commands_dir = tmp_path / ".forge" / "commands"
|
|
assert commands_dir.is_dir()
|
|
|
|
# Derive expected command names from the Forge command templates so the test
|
|
# stays in sync if templates are added/removed.
|
|
templates = forge.list_command_templates()
|
|
expected_commands = {t.stem for t in templates}
|
|
assert len(expected_commands) > 0, "No command templates found"
|
|
|
|
# Check generated files match templates
|
|
command_files = sorted(commands_dir.glob("speckit.*.md"))
|
|
assert len(command_files) == len(expected_commands)
|
|
actual_commands = {f.name.removeprefix("speckit.").removesuffix(".md") for f in command_files}
|
|
assert actual_commands == expected_commands
|
|
|
|
def test_templates_are_processed(self, tmp_path):
|
|
import re
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
forge = ForgeIntegration()
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
forge.setup(tmp_path, m)
|
|
commands_dir = tmp_path / ".forge" / "commands"
|
|
for cmd_file in commands_dir.glob("speckit.*.md"):
|
|
content = cmd_file.read_text(encoding="utf-8")
|
|
# Check standard replacements
|
|
assert "{SCRIPT}" not in content, f"{cmd_file.name} has unprocessed {{SCRIPT}}"
|
|
assert "__AGENT__" not in content, f"{cmd_file.name} has unprocessed __AGENT__"
|
|
assert "{ARGS}" not in content, f"{cmd_file.name} has unprocessed {{ARGS}}"
|
|
assert "__SPECKIT_COMMAND_" not in content, f"{cmd_file.name} has unprocessed __SPECKIT_COMMAND_*__"
|
|
# Check Forge-specific: $ARGUMENTS should be replaced with {{parameters}}
|
|
assert "$ARGUMENTS" not in content, f"{cmd_file.name} has unprocessed $ARGUMENTS"
|
|
# Frontmatter sections should be stripped
|
|
assert "\nscripts:\n" not in content
|
|
# Check Forge-specific: command references use hyphen notation, not dot notation
|
|
assert not re.search(r"/speckit\.[a-z]", content), (
|
|
f"{cmd_file.name} contains dot-notation command reference (/speckit.<cmd>); "
|
|
"Forge requires hyphen notation (/speckit-<cmd>) for ZSH compatibility"
|
|
)
|
|
|
|
def test_plan_references_correct_context_file(self, tmp_path):
|
|
"""The generated plan command must reference forge's context file."""
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
forge = ForgeIntegration()
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
forge.setup(tmp_path, m)
|
|
plan_file = tmp_path / ".forge" / "commands" / "speckit.plan.md"
|
|
assert plan_file.exists()
|
|
content = plan_file.read_text(encoding="utf-8")
|
|
assert forge.context_file in content, (
|
|
f"Plan command should reference {forge.context_file!r}"
|
|
)
|
|
assert "__CONTEXT_FILE__" not in content
|
|
|
|
def test_forge_specific_transformations(self, tmp_path):
|
|
"""Test Forge-specific processing: name injection and handoffs stripping."""
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
from specify_cli.agents import CommandRegistrar
|
|
forge = ForgeIntegration()
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
forge.setup(tmp_path, m)
|
|
commands_dir = tmp_path / ".forge" / "commands"
|
|
|
|
registrar = CommandRegistrar()
|
|
for cmd_file in commands_dir.glob("speckit.*.md"):
|
|
content = cmd_file.read_text(encoding="utf-8")
|
|
frontmatter, _ = registrar.parse_frontmatter(content)
|
|
|
|
# Check that name field is injected in frontmatter
|
|
assert "name" in frontmatter, f"{cmd_file.name} missing injected 'name' field in frontmatter"
|
|
|
|
# Check that handoffs frontmatter key is stripped
|
|
assert "handoffs" not in frontmatter, f"{cmd_file.name} has unstripped 'handoffs' key in frontmatter"
|
|
|
|
def test_uses_parameters_placeholder(self, tmp_path):
|
|
"""Verify Forge replaces $ARGUMENTS with {{parameters}} in generated files."""
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
forge = ForgeIntegration()
|
|
|
|
# The registrar_config should specify {{parameters}}
|
|
assert forge.registrar_config["args"] == "{{parameters}}"
|
|
|
|
# Generate files and verify $ARGUMENTS is replaced with {{parameters}}
|
|
from specify_cli.integrations.manifest import IntegrationManifest
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
forge.setup(tmp_path, m)
|
|
commands_dir = tmp_path / ".forge" / "commands"
|
|
|
|
# Check all generated command files
|
|
for cmd_file in commands_dir.glob("speckit.*.md"):
|
|
content = cmd_file.read_text(encoding="utf-8")
|
|
# $ARGUMENTS should be replaced with {{parameters}}
|
|
assert "$ARGUMENTS" not in content, (
|
|
f"{cmd_file.name} still contains $ARGUMENTS - it should be replaced with {{{{parameters}}}}"
|
|
)
|
|
# At least some files should have {{parameters}} (those with user input sections)
|
|
# We'll check the checklist file specifically as it has a User Input section
|
|
|
|
# Verify checklist specifically has {{parameters}} in the User Input section
|
|
checklist = commands_dir / "speckit.checklist.md"
|
|
if checklist.exists():
|
|
content = checklist.read_text(encoding="utf-8")
|
|
assert "{{parameters}}" in content, (
|
|
"checklist should contain {{parameters}} in User Input section"
|
|
)
|
|
|
|
def test_command_refs_use_hyphen_notation(self, tmp_path):
|
|
"""Verify all generated Forge command files use /speckit-foo, not /speckit.foo."""
|
|
import re
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
forge = ForgeIntegration()
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
forge.setup(tmp_path, m)
|
|
commands_dir = tmp_path / ".forge" / "commands"
|
|
|
|
files_with_refs = []
|
|
files_with_dot_refs = []
|
|
for cmd_file in commands_dir.glob("speckit.*.md"):
|
|
content = cmd_file.read_text(encoding="utf-8")
|
|
if re.search(r"/speckit-[a-z]", content):
|
|
files_with_refs.append(cmd_file.name)
|
|
if re.search(r"/speckit\.[a-z]", content):
|
|
files_with_dot_refs.append(cmd_file.name)
|
|
|
|
assert files_with_dot_refs == [], (
|
|
f"Files contain dot-notation command references: {files_with_dot_refs}. "
|
|
"Forge requires hyphen notation (/speckit-<cmd>) for ZSH compatibility."
|
|
)
|
|
assert len(files_with_refs) > 0, (
|
|
"Expected at least one generated Forge command to contain /speckit-<cmd> reference, "
|
|
"but none were found. Check that __SPECKIT_COMMAND_*__ tokens are being resolved."
|
|
)
|
|
|
|
def test_name_field_uses_hyphenated_format(self, tmp_path):
|
|
"""Verify that injected name fields use hyphenated format (speckit-plan, not speckit.plan)."""
|
|
from specify_cli.integrations.forge import ForgeIntegration
|
|
from specify_cli.agents import CommandRegistrar
|
|
forge = ForgeIntegration()
|
|
m = IntegrationManifest("forge", tmp_path)
|
|
forge.setup(tmp_path, m)
|
|
commands_dir = tmp_path / ".forge" / "commands"
|
|
|
|
# Check that name fields use hyphenated format
|
|
registrar = CommandRegistrar()
|
|
for cmd_file in commands_dir.glob("speckit.*.md"):
|
|
content = cmd_file.read_text(encoding="utf-8")
|
|
# Extract the name field from frontmatter using the parser
|
|
frontmatter, _ = registrar.parse_frontmatter(content)
|
|
assert "name" in frontmatter, (
|
|
f"{cmd_file.name} missing injected 'name' field in frontmatter"
|
|
)
|
|
name_value = frontmatter["name"]
|
|
# Name should use hyphens, not dots
|
|
assert "." not in name_value, (
|
|
f"{cmd_file.name} has name field with dots: {name_value} "
|
|
f"(should use hyphens for Forge/ZSH compatibility)"
|
|
)
|
|
assert name_value.startswith("speckit-"), (
|
|
f"{cmd_file.name} name field should start with 'speckit-': {name_value}"
|
|
)
|
|
|
|
|
|
class TestForgeCommandRegistrar:
|
|
"""Test CommandRegistrar's Forge-specific name formatting."""
|
|
|
|
def test_registrar_formats_extension_command_names_for_forge(self, tmp_path):
|
|
"""Verify CommandRegistrar converts dot notation to hyphens for Forge."""
|
|
from specify_cli.agents import CommandRegistrar
|
|
|
|
# Create a mock extension command file
|
|
ext_dir = tmp_path / "extension"
|
|
ext_dir.mkdir()
|
|
cmd_dir = ext_dir / "commands"
|
|
cmd_dir.mkdir()
|
|
|
|
# Create a test command with dot notation name
|
|
cmd_file = cmd_dir / "example.md"
|
|
cmd_file.write_text(
|
|
"---\n"
|
|
"description: Test extension command\n"
|
|
"---\n\n"
|
|
"Test content with $ARGUMENTS\n",
|
|
encoding="utf-8"
|
|
)
|
|
|
|
# Register with Forge
|
|
registrar = CommandRegistrar()
|
|
commands = [
|
|
{
|
|
"name": "speckit.my-extension.example",
|
|
"file": "commands/example.md"
|
|
}
|
|
]
|
|
|
|
registered = registrar.register_commands(
|
|
"forge",
|
|
commands,
|
|
"test-extension",
|
|
ext_dir,
|
|
tmp_path
|
|
)
|
|
|
|
# Verify registration succeeded
|
|
assert "speckit.my-extension.example" in registered
|
|
|
|
# Check the generated file has hyphenated name in frontmatter
|
|
forge_cmd = tmp_path / ".forge" / "commands" / "speckit.my-extension.example.md"
|
|
assert forge_cmd.exists()
|
|
|
|
content = forge_cmd.read_text(encoding="utf-8")
|
|
# Parse frontmatter to validate name field precisely
|
|
frontmatter, _ = registrar.parse_frontmatter(content)
|
|
assert "name" in frontmatter, "name field should be injected in frontmatter"
|
|
# Name field should use hyphens, not dots
|
|
assert frontmatter["name"] == "speckit-my-extension-example"
|
|
|
|
def test_registrar_formats_alias_names_for_forge(self, tmp_path):
|
|
"""Verify CommandRegistrar converts alias names to hyphens for Forge."""
|
|
from specify_cli.agents import CommandRegistrar
|
|
|
|
# Create a mock extension command file
|
|
ext_dir = tmp_path / "extension"
|
|
ext_dir.mkdir()
|
|
cmd_dir = ext_dir / "commands"
|
|
cmd_dir.mkdir()
|
|
|
|
cmd_file = cmd_dir / "example.md"
|
|
cmd_file.write_text(
|
|
"---\n"
|
|
"description: Test command with alias\n"
|
|
"---\n\n"
|
|
"Test content\n",
|
|
encoding="utf-8"
|
|
)
|
|
|
|
# Register with Forge including an alias
|
|
registrar = CommandRegistrar()
|
|
commands = [
|
|
{
|
|
"name": "speckit.my-extension.example",
|
|
"file": "commands/example.md",
|
|
"aliases": ["speckit.my-extension.ex"]
|
|
}
|
|
]
|
|
|
|
registrar.register_commands(
|
|
"forge",
|
|
commands,
|
|
"test-extension",
|
|
ext_dir,
|
|
tmp_path
|
|
)
|
|
|
|
# Check the alias file has hyphenated name in frontmatter
|
|
alias_file = tmp_path / ".forge" / "commands" / "speckit.my-extension.ex.md"
|
|
assert alias_file.exists()
|
|
|
|
content = alias_file.read_text(encoding="utf-8")
|
|
# Parse frontmatter to validate alias name field precisely
|
|
frontmatter, _ = registrar.parse_frontmatter(content)
|
|
assert "name" in frontmatter, "name field should be injected in alias frontmatter"
|
|
# Alias name field should also use hyphens
|
|
assert frontmatter["name"] == "speckit-my-extension-ex"
|
|
|
|
def test_registrar_does_not_affect_other_agents(self, tmp_path):
|
|
"""Verify format_name callback is Forge-specific and doesn't affect other agents."""
|
|
from specify_cli.agents import CommandRegistrar
|
|
|
|
# Create a mock extension command file
|
|
ext_dir = tmp_path / "extension"
|
|
ext_dir.mkdir()
|
|
cmd_dir = ext_dir / "commands"
|
|
cmd_dir.mkdir()
|
|
|
|
cmd_file = cmd_dir / "example.md"
|
|
cmd_file.write_text(
|
|
"---\n"
|
|
"description: Test command\n"
|
|
"---\n\n"
|
|
"Test content with $ARGUMENTS\n",
|
|
encoding="utf-8"
|
|
)
|
|
|
|
# Register with Windsurf (standard markdown agent without inject_name)
|
|
registrar = CommandRegistrar()
|
|
commands = [
|
|
{
|
|
"name": "speckit.my-extension.example",
|
|
"file": "commands/example.md"
|
|
}
|
|
]
|
|
|
|
registrar.register_commands(
|
|
"windsurf",
|
|
commands,
|
|
"test-extension",
|
|
ext_dir,
|
|
tmp_path
|
|
)
|
|
|
|
# Windsurf uses standard markdown format without name injection.
|
|
# The format_name callback should not be invoked for non-Forge agents.
|
|
windsurf_cmd = tmp_path / ".windsurf" / "workflows" / "speckit.my-extension.example.md"
|
|
assert windsurf_cmd.exists()
|
|
|
|
content = windsurf_cmd.read_text(encoding="utf-8")
|
|
# Windsurf should NOT have a name field injected
|
|
assert "name:" not in content, (
|
|
"Windsurf should not inject name field - format_name callback should be Forge-only"
|
|
)
|
|
|
|
def test_git_extension_command_uses_hyphen_notation(self, tmp_path):
|
|
"""Verify the git extension's feature command uses /speckit-specify (not /speckit.specify) for Forge."""
|
|
from pathlib import Path
|
|
from specify_cli.agents import CommandRegistrar
|
|
|
|
# Locate the real git extension command source file
|
|
repo_root = Path(__file__).resolve().parent.parent.parent
|
|
ext_dir = repo_root / "extensions" / "git"
|
|
cmd_source = ext_dir / "commands" / "speckit.git.feature.md"
|
|
assert cmd_source.exists(), (
|
|
f"Git extension command source not found at {cmd_source}. "
|
|
"Ensure extensions/git/commands/speckit.git.feature.md exists."
|
|
)
|
|
|
|
registrar = CommandRegistrar()
|
|
commands = [
|
|
{
|
|
"name": "speckit.git.feature",
|
|
"file": "commands/speckit.git.feature.md",
|
|
}
|
|
]
|
|
|
|
registered = registrar.register_commands(
|
|
"forge",
|
|
commands,
|
|
"git",
|
|
ext_dir,
|
|
tmp_path,
|
|
)
|
|
|
|
assert "speckit.git.feature" in registered
|
|
|
|
forge_cmd = tmp_path / ".forge" / "commands" / "speckit.git.feature.md"
|
|
assert forge_cmd.exists(), "Expected Forge command file was not created"
|
|
|
|
content = forge_cmd.read_text(encoding="utf-8")
|
|
assert "/speckit-specify" in content, (
|
|
"Expected '/speckit-specify' (hyphen) in generated Forge git.feature command body, "
|
|
"but it was not found. Check that __SPECKIT_COMMAND_SPECIFY__ is resolved correctly."
|
|
)
|
|
assert "/speckit.specify" not in content, (
|
|
"Found '/speckit.specify' (dot notation) in generated Forge git.feature command body. "
|
|
"Forge requires hyphen notation for ZSH compatibility."
|
|
)
|