Skip to content

Commit 96e669b

Browse files
committed
fix(integrations): preserve dollar refs everywhere
Use agent-native invocation prefixes in extension command registration and dynamic shared-script command hints. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19
1 parent 5ec0599 commit 96e669b

4 files changed

Lines changed: 64 additions & 10 deletions

File tree

src/specify_cli/agents.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import yaml
1616

1717
from ._init_options import is_ai_skills_enabled, load_init_options
18+
from ._invocation_style import is_dollar_skills_agent
1819
from ._toml_string import escape_toml_basic as _escape_toml_basic
1920
from ._toml_string import has_illegal_toml_control as _has_illegal_toml_control
2021
from ._utils import relative_extension_path_violation
@@ -659,17 +660,20 @@ def register_commands(
659660
# correct when a stale ``.bob/skills`` directory coexists with
660661
# ``.bob/commands``.
661662
_sep = agent_config.get("invoke_separator", ".")
663+
registrar_writes_skills = agent_config.get("extension") == "/SKILL.md"
662664
try:
663665
from specify_cli.integrations import get_integration # noqa: PLC0415
664666

665667
_integ = get_integration(agent_name)
666668
if _integ is not None:
667-
registrar_writes_skills = (
668-
agent_config.get("extension") == "/SKILL.md"
669-
)
670669
_sep = _integ.invoke_separator_for_mode(registrar_writes_skills)
671670
except Exception:
672671
pass
672+
_prefix = (
673+
"$"
674+
if is_dollar_skills_agent(agent_name, registrar_writes_skills)
675+
else "/"
676+
)
673677

674678
for cmd_info in commands:
675679
cmd_name = cmd_info["name"]
@@ -755,7 +759,7 @@ def register_commands(
755759
# (base.py itself imports CommandRegistrar lazily).
756760
from specify_cli.integrations.base import IntegrationBase # noqa: PLC0415
757761

758-
body = IntegrationBase.resolve_command_refs(body, _sep)
762+
body = IntegrationBase.resolve_command_refs(body, _sep, _prefix)
759763

760764
output_name = self._compute_output_name(agent_name, cmd_name, agent_config)
761765

src/specify_cli/shared_infra.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,25 +274,29 @@ def _write_shared_bytes(
274274
)
275275

276276

277-
def _format_speckit_command(command_name: str, separator: str) -> str:
277+
def _format_speckit_command(
278+
command_name: str, separator: str, prefix: str = "/"
279+
) -> str:
278280
name = command_name.strip().lstrip("/")
279281
if name.startswith("speckit."):
280282
name = name[len("speckit.") :]
281283
elif name.startswith("speckit-"):
282284
name = name[len("speckit-") :]
283285
name = name.replace(".", separator)
284-
return f"/speckit{separator}{name}"
286+
return f"{prefix}speckit{separator}{name}"
285287

286288

287-
def _resolve_dynamic_command_refs(content: str, separator: str) -> str:
289+
def _resolve_dynamic_command_refs(
290+
content: str, separator: str, prefix: str = "/"
291+
) -> str:
288292
"""Render script runtime command helpers for managed shared infra copies."""
289293

290294
content = _BASH_FORMAT_COMMAND_RE.sub(
291-
lambda match: _format_speckit_command(match.group(2), separator),
295+
lambda match: _format_speckit_command(match.group(2), separator, prefix),
292296
content,
293297
)
294298
return _POWERSHELL_FORMAT_COMMAND_RE.sub(
295-
lambda match: f"'{_format_speckit_command(match.group(2), separator)}'",
299+
lambda match: f"'{_format_speckit_command(match.group(2), separator, prefix)}'",
296300
content,
297301
)
298302

@@ -523,7 +527,9 @@ def _ensure_or_bucket_dir(directory: Path) -> bool:
523527
content = IntegrationBase.resolve_command_refs(
524528
content, invoke_separator, invoke_prefix
525529
)
526-
content = _resolve_dynamic_command_refs(content, invoke_separator)
530+
content = _resolve_dynamic_command_refs(
531+
content, invoke_separator, invoke_prefix
532+
)
527533
planned_copies.append(
528534
(
529535
dst_path,

tests/integrations/test_cli.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1237,6 +1237,27 @@ def test_hyphen_separator_in_shared_scripts(self, tmp_path, script_type):
12371237
assert "/speckit.plan" not in content
12381238
assert "/speckit.tasks" not in content
12391239

1240+
@pytest.mark.parametrize("script_type", ["sh", "ps"])
1241+
def test_dollar_prefix_in_shared_scripts(self, tmp_path, script_type):
1242+
"""Dollar-style skills agents get native prefixes in shared script hints."""
1243+
from specify_cli import _install_shared_infra
1244+
1245+
project = tmp_path / f"dollar-script-{script_type}"
1246+
project.mkdir()
1247+
(project / ".specify").mkdir()
1248+
1249+
_install_shared_infra(
1250+
project, script_type, invoke_separator="-", invoke_prefix="$"
1251+
)
1252+
1253+
content = self._combined_script_content(project, script_type)
1254+
assert "$speckit-specify" in content
1255+
assert "$speckit-plan" in content
1256+
assert "$speckit-tasks" in content
1257+
assert "/speckit-specify" not in content
1258+
assert "/speckit-plan" not in content
1259+
assert "/speckit-tasks" not in content
1260+
12401261
def test_full_init_claude_resolves_page_templates(self, tmp_path):
12411262
"""Full CLI init with Claude (skills agent) produces hyphen refs in page templates."""
12421263
from typer.testing import CliRunner

tests/test_extensions.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2812,6 +2812,29 @@ def test_codex_skill_registration_writes_skill_frontmatter(self, extension_dir,
28122812
assert "source: test-ext:commands/hello.md" in content
28132813
assert "<!-- Extension:" not in content
28142814

2815+
def test_codex_skill_registration_uses_dollar_command_refs(
2816+
self, extension_dir, project_dir
2817+
):
2818+
"""Codex extension skills use the native dollar invocation prefix."""
2819+
skills_dir = project_dir / ".agents" / "skills"
2820+
skills_dir.mkdir(parents=True)
2821+
command = extension_dir / "commands" / "hello.md"
2822+
command.write_text(
2823+
"---\ndescription: Test hello command\n---\n\nRun __SPECKIT_COMMAND_PLAN__.",
2824+
encoding="utf-8",
2825+
)
2826+
2827+
manifest = ExtensionManifest(extension_dir / "extension.yml")
2828+
registrar = CommandRegistrar()
2829+
registrar.register_commands_for_agent(
2830+
"codex", manifest, extension_dir, project_dir
2831+
)
2832+
2833+
skill_file = skills_dir / "speckit-test-ext-hello" / "SKILL.md"
2834+
content = skill_file.read_text(encoding="utf-8")
2835+
assert "$speckit-plan" in content
2836+
assert "/speckit-plan" not in content
2837+
28152838
def test_codex_skill_registration_resolves_script_placeholders(self, project_dir, temp_dir):
28162839
"""Codex SKILL.md overrides should resolve script placeholders."""
28172840
import yaml

0 commit comments

Comments
 (0)