Skip to content

Commit 25c36e5

Browse files
committed
fix(integrations): preserve shared invocation prefix
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 a31dbbd commit 25c36e5

7 files changed

Lines changed: 109 additions & 3 deletions

File tree

src/specify_cli/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def _refresh_shared_templates(
114114
project_path: Path,
115115
*,
116116
invoke_separator: str,
117+
invoke_prefix: str = "/",
117118
force: bool = False,
118119
) -> None:
119120
"""Refresh default-sensitive shared templates without touching scripts."""
@@ -124,6 +125,7 @@ def _refresh_shared_templates(
124125
repo_root=_repo_root(),
125126
console=console,
126127
invoke_separator=invoke_separator,
128+
invoke_prefix=invoke_prefix,
127129
force=force,
128130
)
129131

@@ -134,6 +136,7 @@ def _install_shared_infra(
134136
tracker: StepTracker | None = None,
135137
force: bool = False,
136138
invoke_separator: str = ".",
139+
invoke_prefix: str = "/",
137140
refresh_managed: bool = False,
138141
refresh_hint: str | None = None,
139142
) -> bool:
@@ -177,6 +180,7 @@ def _install_shared_infra(
177180
console=console,
178181
force=force,
179182
invoke_separator=invoke_separator,
183+
invoke_prefix=invoke_prefix,
180184
refresh_managed=refresh_managed,
181185
refresh_hint=refresh_hint,
182186
)
@@ -188,6 +192,7 @@ def _install_shared_infra_or_exit(
188192
tracker: StepTracker | None = None,
189193
force: bool = False,
190194
invoke_separator: str = ".",
195+
invoke_prefix: str = "/",
191196
refresh_managed: bool = False,
192197
refresh_hint: str | None = None,
193198
) -> bool:
@@ -198,6 +203,7 @@ def _install_shared_infra_or_exit(
198203
tracker=tracker,
199204
force=force,
200205
invoke_separator=invoke_separator,
206+
invoke_prefix=invoke_prefix,
201207
refresh_managed=refresh_managed,
202208
refresh_hint=refresh_hint,
203209
)

src/specify_cli/commands/init.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
get_speckit_version,
2424
)
2525
from .._console import StepTracker, console, select_with_arrows, show_banner
26+
from .._invocation_style import is_dollar_skills_agent
2627
from .._utils import check_tool
2728

2829

@@ -481,6 +482,16 @@ def init(
481482
invoke_separator=resolved_integration.effective_invoke_separator(
482483
integration_parsed_options, project_root=project_path
483484
),
485+
invoke_prefix=(
486+
"$"
487+
if is_dollar_skills_agent(
488+
resolved_integration.key,
489+
resolved_integration.is_skills_mode(
490+
integration_parsed_options, project_path
491+
),
492+
)
493+
else "/"
494+
),
484495
)
485496
tracker.complete(
486497
"shared-infra", f"scripts ({selected_script}) + templates"

src/specify_cli/integrations/_helpers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from .._agent_config import SCRIPT_TYPE_CHOICES
1111
from .._console import console
12+
from .._invocation_style import is_dollar_skills_agent
1213
from ..integration_runtime import (
1314
invoke_separator_for_integration as _invoke_separator_for_integration,
1415
resolve_integration_options as _resolve_integration_options_impl,
@@ -332,6 +333,13 @@ def _set_default_integration(
332333
integration, {"integration_settings": settings}, key, parsed_options,
333334
project_root=project_root,
334335
),
336+
invoke_prefix=(
337+
"$"
338+
if is_dollar_skills_agent(
339+
key, integration.is_skills_mode(parsed_options, project_root)
340+
)
341+
else "/"
342+
),
335343
force=refresh_templates_force,
336344
refresh_managed=True,
337345
refresh_hint=refresh_hint,

src/specify_cli/integrations/_migrate_commands.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import typer
99

1010
from .._console import console
11+
from .._invocation_style import is_dollar_skills_agent
1112
from ..integration_runtime import (
1213
invoke_separator_for_integration as _invoke_separator_for_integration,
1314
with_integration_setting as _with_integration_setting,
@@ -327,6 +328,13 @@ def integration_switch(
327328
target_integration, current, target, parsed_options,
328329
project_root=project_root,
329330
),
331+
invoke_prefix=(
332+
"$"
333+
if is_dollar_skills_agent(
334+
target, target_integration.is_skills_mode(parsed_options, project_root)
335+
)
336+
else "/"
337+
),
330338
refresh_hint=(
331339
"To overwrite customizations, re-run with "
332340
"[cyan]specify integration switch ... --refresh-shared-infra[/cyan]."
@@ -557,6 +565,14 @@ def integration_upgrade(
557565
infra_integration, current, infra_key, infra_parsed,
558566
project_root=project_root,
559567
),
568+
invoke_prefix=(
569+
"$"
570+
if is_dollar_skills_agent(
571+
infra_key,
572+
infra_integration.is_skills_mode(infra_parsed, project_root),
573+
)
574+
else "/"
575+
),
560576
)
561577
if os.name != "nt":
562578
from .. import ensure_executable_scripts
@@ -592,6 +608,13 @@ def integration_upgrade(
592608
integration, {"integration_settings": settings}, key, parsed_options,
593609
project_root=project_root,
594610
),
611+
invoke_prefix=(
612+
"$"
613+
if is_dollar_skills_agent(
614+
key, integration.is_skills_mode(parsed_options, project_root)
615+
)
616+
else "/"
617+
),
595618
force=force,
596619
refresh_managed=True,
597620
)

src/specify_cli/shared_infra.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ def refresh_shared_templates(
305305
repo_root: Path,
306306
console: Any,
307307
invoke_separator: str,
308+
invoke_prefix: str = "/",
308309
force: bool = False,
309310
) -> None:
310311
"""Refresh default-sensitive shared templates without touching scripts."""
@@ -336,7 +337,9 @@ def refresh_shared_templates(
336337
continue
337338

338339
content = src.read_text(encoding="utf-8")
339-
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
340+
content = IntegrationBase.resolve_command_refs(
341+
content, invoke_separator, invoke_prefix
342+
)
340343
planned_updates.append((dst, rel, content))
341344

342345
for dst, rel, content in planned_updates:
@@ -363,6 +366,7 @@ def install_shared_infra(
363366
console: Any,
364367
force: bool = False,
365368
invoke_separator: str = ".",
369+
invoke_prefix: str = "/",
366370
refresh_managed: bool = False,
367371
refresh_hint: str | None = None,
368372
) -> bool:
@@ -516,7 +520,9 @@ def _ensure_or_bucket_dir(directory: Path) -> bool:
516520
if not _ensure_or_bucket_dir(dst_path.parent):
517521
continue
518522
content = src_path.read_text(encoding="utf-8")
519-
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
523+
content = IntegrationBase.resolve_command_refs(
524+
content, invoke_separator, invoke_prefix
525+
)
520526
content = _resolve_dynamic_command_refs(content, invoke_separator)
521527
planned_copies.append(
522528
(
@@ -566,7 +572,9 @@ def _ensure_or_bucket_dir(directory: Path) -> bool:
566572
continue
567573

568574
content = src.read_text(encoding="utf-8")
569-
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
575+
content = IntegrationBase.resolve_command_refs(
576+
content, invoke_separator, invoke_prefix
577+
)
570578
planned_templates.append((dst, rel, content))
571579

572580
for dst_path, rel, content, mode in planned_copies:

tests/integrations/test_cli.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,23 @@ def test_hyphen_separator_in_page_templates(self, tmp_path):
11801180
assert "__SPECKIT_COMMAND_" not in content
11811181
assert "/speckit-tasks" in content
11821182

1183+
def test_dollar_prefix_in_page_templates(self, tmp_path):
1184+
"""Dollar-style skills agents get $speckit-<name> in page templates."""
1185+
from specify_cli import _install_shared_infra
1186+
1187+
project = tmp_path / "dollar-test"
1188+
project.mkdir()
1189+
(project / ".specify").mkdir()
1190+
1191+
_install_shared_infra(
1192+
project, "sh", invoke_separator="-", invoke_prefix="$"
1193+
)
1194+
1195+
plan = project / ".specify" / "templates" / "plan-template.md"
1196+
content = plan.read_text(encoding="utf-8")
1197+
assert "$speckit-plan" in content
1198+
assert "/speckit-plan" not in content
1199+
11831200
@pytest.mark.parametrize("script_type", ["sh", "ps"])
11841201
def test_dot_separator_in_shared_scripts(self, tmp_path, script_type):
11851202
"""Markdown agents get /speckit.<name> in shared script hints."""

tests/test_presets.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4090,6 +4090,39 @@ def test_restore_skill_resolves_command_refs(self, project_dir, temp_dir):
40904090
assert "__SPECKIT_COMMAND_" not in content, "raw command token leaked on restore"
40914091
assert "/speckit-plan" in content
40924092

4093+
def test_restore_skill_preserves_dollar_command_refs(self, project_dir, temp_dir):
4094+
"""Dollar-style core refs remain native when a preset skill is removed."""
4095+
from specify_cli.integrations.base import IntegrationBase
4096+
4097+
self._write_init_options(project_dir, ai="zcode")
4098+
skills_dir = project_dir / ".zcode" / "skills"
4099+
self._create_skill(skills_dir, "speckit-specify")
4100+
4101+
core_cmds = project_dir / ".specify" / "templates" / "commands"
4102+
core_cmds.mkdir(parents=True, exist_ok=True)
4103+
raw_core = (
4104+
"---\ndescription: Core specify\n---\n\n"
4105+
"Then run `__SPECKIT_COMMAND_PLAN__`.\n"
4106+
)
4107+
(core_cmds / "specify.md").write_text(
4108+
IntegrationBase.resolve_command_refs(raw_core, "-", "$")
4109+
)
4110+
4111+
preset_dir = self._create_command_preset(
4112+
temp_dir,
4113+
"dollar-cmdref-restore",
4114+
"speckit.specify",
4115+
"Override specify",
4116+
"Override body\n",
4117+
)
4118+
manager = PresetManager(project_dir)
4119+
manager.install_from_directory(preset_dir, "0.1.5")
4120+
manager.remove("dollar-cmdref-restore")
4121+
4122+
content = (skills_dir / "speckit-specify" / "SKILL.md").read_text()
4123+
assert "$speckit-plan" in content
4124+
assert "/speckit-plan" not in content
4125+
40934126
def test_reconcile_override_skill_resolves_command_refs(self, project_dir, temp_dir):
40944127
"""Reconcile's project-override restore must resolve command tokens (issue #2717).
40954128

0 commit comments

Comments
 (0)