mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(integrations): preserve native skill invocation prefixes (#3663)
* fix(integrations): use native dollar skill invocations 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 * fix(integrations): preserve skill post-process idempotence 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 * fix(integrations): preserve literal skill invocations Resolve generated command references with the active agent prefix instead of rewriting all slash-form text during post-processing. 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 * 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 * fix(integrations): preserve install invocation prefix Pass dollar-style skill prefixes through bare-project integration installation and cover the shared template output. 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 * 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 * fix(shared-infra): preserve dollar command hints Escape dollar-prefixed commands embedded in Bash strings and propagate the native prefix into installed Python command helpers. 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 * fix(shared-infra): render native helper prefixes Rewrite installed Bash and PowerShell formatter return expressions so direct callers receive the selected integration's native 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 * fix(skills): use invocation-neutral hook guidance Describe hook-derived references as command invocations so dollar-prefixed skills do not receive contradictory slash-command terminology. 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 * test(integrations): expect native fallback invocation 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 * refactor(integrations): centralize invocation prefix selection 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 * fix(integrations): add Kimi /skill: prefix and fix docstrings - Add SKILL_COLON_AGENTS frozenset and get_invocation_prefix() to _invocation_style.py so Kimi resolves to '/skill:' in skills mode - Switch invoke_prefix_for_integration() to use get_invocation_prefix() instead of the binary dollar/slash check - Update post_process_skill_content docstring (base.py) to cover both slash and dollar native invocation forms - Update _resolve_command_refs_in_skill docstring (presets/__init__.py) to document the dollar-prefixed result alongside slash forms Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * fix(agents): use get_invocation_prefix for Kimi in register_commands Replace the binary is_dollar_skills_agent ternary with get_invocation_prefix so that __SPECKIT_COMMAND_*__ tokens in Kimi skill files resolve to /skill:speckit-<name> rather than /speckit-<name>. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * fix(agents): remove unused is_dollar_skills_agent import Leftover from replacing the inline ternary with get_invocation_prefix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * fix(integrations): use get_invocation_prefix in post_process_skill_content Replaces the binary is_dollar_skills_agent ternary with get_invocation_prefix so that Kimi's hook-command note is injected as /skill:speckit-git-commit from the start. This keeps _inject_hook_command_note idempotent for Kimi: the previous note with its native prefix now matches on repeated passes, preventing duplicate note injection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * fix(presets): use get_invocation_prefix in _resolve_skill_command_refs Replace the binary is_dollar_skills_agent ternary with get_invocation_prefix so Kimi tokens resolve to /skill:speckit-* directly rather than /speckit-* (which previously relied on the broad post-process body replacement). Also fix test_restore_skill_preserves_dollar_command_refs to write raw_core with the unresolved __SPECKIT_COMMAND_PLAN__ token, exercising the resolver rather than bypassing it with a pre-resolved string. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * docs(presets): document /skill: form in _resolve_skill_command_refs Add /skill:speckit-<cmd> to the docstring so the contract covers all three native prefix forms returned by get_invocation_prefix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * test(integrations): add Kimi /skill: prefix coverage - test_skill_colon_prefix_core_command: resolve_command_refs with /skill: prefix - test_get_invocation_prefix_skill_colon: get_invocation_prefix returns /skill: for kimi (skills), / for kimi (non-skills), $ for codex, / for claude - test_kimi_skill_post_processing_is_idempotent: verifies Kimi's hook-command note is injected with /skill: prefix and does not duplicate on re-runs - test_installed_bash_formatter_uses_skill_colon_prefix: shared-infra bash formatter outputs /skill:speckit-plan when installed with /skill: prefix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * fix(kimi): use get_invocation_prefix in process_template, remove broad replacement process_template() was still using a binary is_dollar_skills_agent ternary to select between dollar and slash prefix, so Kimi tokens were emitted as /speckit-* and then corrected by a broad .replace('/speckit-', '/skill:speckit-') in KimiIntegration.post_process_skill_content(). That broad replacement would also rewrite any literal /speckit-* text in generated skill content, contrary to the PR's token-only behavior. - Use get_invocation_prefix(agent_name, invoke_separator == '-') in process_template() so Kimi tokens are emitted as /skill:speckit-* directly. - Remove the broad .replace() from KimiIntegration.post_process_skill_content(); it is now a no-op (tokens are already correctly prefixed at source). - Add test_process_template_kimi_uses_skill_colon_prefix to guard the fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28
This commit is contained in:
@@ -114,6 +114,7 @@ def _refresh_shared_templates(
|
||||
project_path: Path,
|
||||
*,
|
||||
invoke_separator: str,
|
||||
invoke_prefix: str = "/",
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
"""Refresh default-sensitive shared templates without touching scripts."""
|
||||
@@ -124,6 +125,7 @@ def _refresh_shared_templates(
|
||||
repo_root=_repo_root(),
|
||||
console=console,
|
||||
invoke_separator=invoke_separator,
|
||||
invoke_prefix=invoke_prefix,
|
||||
force=force,
|
||||
)
|
||||
|
||||
@@ -134,6 +136,7 @@ def _install_shared_infra(
|
||||
tracker: StepTracker | None = None,
|
||||
force: bool = False,
|
||||
invoke_separator: str = ".",
|
||||
invoke_prefix: str = "/",
|
||||
refresh_managed: bool = False,
|
||||
refresh_hint: str | None = None,
|
||||
) -> bool:
|
||||
@@ -177,6 +180,7 @@ def _install_shared_infra(
|
||||
console=console,
|
||||
force=force,
|
||||
invoke_separator=invoke_separator,
|
||||
invoke_prefix=invoke_prefix,
|
||||
refresh_managed=refresh_managed,
|
||||
refresh_hint=refresh_hint,
|
||||
)
|
||||
@@ -188,6 +192,7 @@ def _install_shared_infra_or_exit(
|
||||
tracker: StepTracker | None = None,
|
||||
force: bool = False,
|
||||
invoke_separator: str = ".",
|
||||
invoke_prefix: str = "/",
|
||||
refresh_managed: bool = False,
|
||||
refresh_hint: str | None = None,
|
||||
) -> bool:
|
||||
@@ -198,6 +203,7 @@ def _install_shared_infra_or_exit(
|
||||
tracker=tracker,
|
||||
force=force,
|
||||
invoke_separator=invoke_separator,
|
||||
invoke_prefix=invoke_prefix,
|
||||
refresh_managed=refresh_managed,
|
||||
refresh_hint=refresh_hint,
|
||||
)
|
||||
|
||||
@@ -29,6 +29,9 @@ CONDITIONAL_SLASH_AGENTS: frozenset[str] = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
# Agents that render /skill:<name> (skill-colon invocation) when in skills mode.
|
||||
SKILL_COLON_AGENTS: frozenset[str] = frozenset({"kimi"})
|
||||
|
||||
|
||||
def is_dollar_skills_agent(selected_ai: str | None, ai_skills_enabled: bool) -> bool:
|
||||
"""Return ``True`` if *selected_ai* uses ``$speckit-<name>`` invocations.
|
||||
@@ -41,6 +44,21 @@ def is_dollar_skills_agent(selected_ai: str | None, ai_skills_enabled: bool) ->
|
||||
return selected_ai in DOLLAR_SKILLS_AGENTS and ai_skills_enabled
|
||||
|
||||
|
||||
def get_invocation_prefix(selected_ai: str | None, ai_skills_enabled: bool) -> str:
|
||||
"""Return the native invocation prefix for *selected_ai* in skills mode.
|
||||
|
||||
Returns ``"$"`` for dollar-skills agents (Codex, ZCode),
|
||||
``"/skill:"`` for skill-colon agents (Kimi), and ``"/"`` for all others.
|
||||
"""
|
||||
if not isinstance(selected_ai, str):
|
||||
return "/"
|
||||
if selected_ai in DOLLAR_SKILLS_AGENTS and ai_skills_enabled:
|
||||
return "$"
|
||||
if selected_ai in SKILL_COLON_AGENTS and ai_skills_enabled:
|
||||
return "/skill:"
|
||||
return "/"
|
||||
|
||||
|
||||
def is_slash_skills_agent(selected_ai: str | None, ai_skills_enabled: bool) -> bool:
|
||||
"""Return ``True`` if *selected_ai* uses ``/speckit-<name>`` invocations.
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from typing import Any, Dict, Iterable, List, Optional
|
||||
import yaml
|
||||
|
||||
from ._init_options import is_ai_skills_enabled, load_init_options
|
||||
from ._invocation_style import get_invocation_prefix
|
||||
from ._toml_string import escape_toml_basic as _escape_toml_basic
|
||||
from ._toml_string import has_illegal_toml_control as _has_illegal_toml_control
|
||||
from ._utils import relative_extension_path_violation
|
||||
@@ -659,17 +660,16 @@ class CommandRegistrar:
|
||||
# correct when a stale ``.bob/skills`` directory coexists with
|
||||
# ``.bob/commands``.
|
||||
_sep = agent_config.get("invoke_separator", ".")
|
||||
registrar_writes_skills = agent_config.get("extension") == "/SKILL.md"
|
||||
try:
|
||||
from specify_cli.integrations import get_integration # noqa: PLC0415
|
||||
|
||||
_integ = get_integration(agent_name)
|
||||
if _integ is not None:
|
||||
registrar_writes_skills = (
|
||||
agent_config.get("extension") == "/SKILL.md"
|
||||
)
|
||||
_sep = _integ.invoke_separator_for_mode(registrar_writes_skills)
|
||||
except Exception:
|
||||
pass
|
||||
_prefix = get_invocation_prefix(agent_name, registrar_writes_skills)
|
||||
|
||||
for cmd_info in commands:
|
||||
cmd_name = cmd_info["name"]
|
||||
@@ -772,7 +772,7 @@ class CommandRegistrar:
|
||||
# (base.py itself imports CommandRegistrar lazily).
|
||||
from specify_cli.integrations.base import IntegrationBase # noqa: PLC0415
|
||||
|
||||
body = IntegrationBase.resolve_command_refs(body, _sep)
|
||||
body = IntegrationBase.resolve_command_refs(body, _sep, _prefix)
|
||||
|
||||
output_name = self._compute_output_name(agent_name, cmd_name, agent_config)
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ def register(app: typer.Typer) -> None:
|
||||
save_init_options,
|
||||
)
|
||||
from ..integration_runtime import (
|
||||
invoke_prefix_for_integration as _invoke_prefix_for_integration,
|
||||
with_integration_setting as _with_integration_setting,
|
||||
)
|
||||
from ..integrations._commands import (
|
||||
@@ -481,6 +482,12 @@ def register(app: typer.Typer) -> None:
|
||||
invoke_separator=resolved_integration.effective_invoke_separator(
|
||||
integration_parsed_options, project_root=project_path
|
||||
),
|
||||
invoke_prefix=_invoke_prefix_for_integration(
|
||||
resolved_integration,
|
||||
resolved_integration.key,
|
||||
integration_parsed_options,
|
||||
project_path,
|
||||
),
|
||||
)
|
||||
tracker.complete(
|
||||
"shared-infra", f"scripts ({selected_script}) + templates"
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from ._invocation_style import get_invocation_prefix
|
||||
from .integration_state import integration_setting, integration_settings
|
||||
|
||||
|
||||
@@ -99,3 +100,14 @@ def invoke_separator_for_integration(
|
||||
return integration.effective_invoke_separator(stored_parsed, project_root)
|
||||
|
||||
return integration.effective_invoke_separator(None, project_root)
|
||||
|
||||
|
||||
def invoke_prefix_for_integration(
|
||||
integration: Any,
|
||||
key: str,
|
||||
parsed_options: dict[str, Any] | None = None,
|
||||
project_root: Any = None,
|
||||
) -> str:
|
||||
"""Resolve the native invocation prefix for an integration's output mode."""
|
||||
skills_mode = integration.is_skills_mode(parsed_options, project_root)
|
||||
return get_invocation_prefix(key, skills_mode)
|
||||
|
||||
@@ -11,6 +11,7 @@ from rich.markup import escape
|
||||
from .._agent_config import SCRIPT_TYPE_CHOICES
|
||||
from .._console import console
|
||||
from ..integration_runtime import (
|
||||
invoke_prefix_for_integration as _invoke_prefix_for_integration,
|
||||
invoke_separator_for_integration as _invoke_separator_for_integration,
|
||||
resolve_integration_options as _resolve_integration_options_impl,
|
||||
with_integration_setting as _with_integration_setting,
|
||||
@@ -333,6 +334,9 @@ def _set_default_integration(
|
||||
integration, {"integration_settings": settings}, key, parsed_options,
|
||||
project_root=project_root,
|
||||
),
|
||||
invoke_prefix=_invoke_prefix_for_integration(
|
||||
integration, key, parsed_options, project_root
|
||||
),
|
||||
force=refresh_templates_force,
|
||||
refresh_managed=True,
|
||||
refresh_hint=refresh_hint,
|
||||
|
||||
@@ -8,6 +8,7 @@ import typer
|
||||
from .._console import console
|
||||
from .._utils import _display_project_path
|
||||
from ..integration_runtime import (
|
||||
invoke_prefix_for_integration as _invoke_prefix_for_integration,
|
||||
invoke_separator_for_integration as _invoke_separator_for_integration,
|
||||
with_integration_setting as _with_integration_setting,
|
||||
)
|
||||
@@ -130,6 +131,9 @@ def integration_install(
|
||||
infra_integration, current, infra_key, infra_parsed,
|
||||
project_root=project_root,
|
||||
),
|
||||
invoke_prefix=_invoke_prefix_for_integration(
|
||||
infra_integration, infra_key, infra_parsed, project_root
|
||||
),
|
||||
)
|
||||
if os.name != "nt":
|
||||
from .. import ensure_executable_scripts
|
||||
|
||||
@@ -9,6 +9,7 @@ import typer
|
||||
|
||||
from .._console import console
|
||||
from ..integration_runtime import (
|
||||
invoke_prefix_for_integration as _invoke_prefix_for_integration,
|
||||
invoke_separator_for_integration as _invoke_separator_for_integration,
|
||||
with_integration_setting as _with_integration_setting,
|
||||
)
|
||||
@@ -447,6 +448,9 @@ def integration_switch(
|
||||
target_integration, current, target, parsed_options,
|
||||
project_root=project_root,
|
||||
),
|
||||
invoke_prefix=_invoke_prefix_for_integration(
|
||||
target_integration, target, parsed_options, project_root
|
||||
),
|
||||
refresh_hint=(
|
||||
"To overwrite customizations, re-run with "
|
||||
"[cyan]specify integration switch ... --refresh-shared-infra[/cyan]."
|
||||
@@ -747,6 +751,9 @@ def integration_upgrade(
|
||||
infra_integration, current, infra_key, infra_parsed,
|
||||
project_root=project_root,
|
||||
),
|
||||
invoke_prefix=_invoke_prefix_for_integration(
|
||||
infra_integration, infra_key, infra_parsed, project_root
|
||||
),
|
||||
)
|
||||
if os.name != "nt":
|
||||
from .. import ensure_executable_scripts
|
||||
@@ -782,6 +789,9 @@ def integration_upgrade(
|
||||
integration, {"integration_settings": settings}, key, parsed_options,
|
||||
project_root=project_root,
|
||||
),
|
||||
invoke_prefix=_invoke_prefix_for_integration(
|
||||
integration, key, parsed_options, project_root
|
||||
),
|
||||
force=force,
|
||||
refresh_managed=True,
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
import yaml
|
||||
|
||||
from .._invocation_style import get_invocation_prefix, is_dollar_skills_agent
|
||||
from .._toml_string import escape_toml_basic as _escape_toml_basic
|
||||
from .._toml_string import has_illegal_toml_control as _has_illegal_toml_control
|
||||
|
||||
@@ -34,7 +35,7 @@ if TYPE_CHECKING:
|
||||
from .manifest import IntegrationManifest
|
||||
|
||||
_HOOK_COMMAND_NOTE = (
|
||||
"- When constructing slash commands from hook command names, "
|
||||
"- When constructing command invocations from hook command names, "
|
||||
"replace dots (`.`) with hyphens (`-`). "
|
||||
"For example, `speckit.git.commit` → `/speckit-git-commit`.\n"
|
||||
)
|
||||
@@ -601,7 +602,9 @@ class IntegrationBase(ABC):
|
||||
return created
|
||||
|
||||
@staticmethod
|
||||
def resolve_command_refs(content: str, separator: str = ".") -> str:
|
||||
def resolve_command_refs(
|
||||
content: str, separator: str = ".", prefix: str = "/"
|
||||
) -> str:
|
||||
"""Replace ``__SPECKIT_COMMAND_<NAME>__`` placeholders with invocations.
|
||||
|
||||
Each placeholder encodes a command name in upper-case with
|
||||
@@ -611,10 +614,16 @@ class IntegrationBase(ABC):
|
||||
|
||||
* ``separator="."`` → ``/speckit.plan``, ``/speckit.git.commit``
|
||||
* ``separator="-"`` → ``/speckit-plan``, ``/speckit-git-commit``
|
||||
|
||||
*prefix* defaults to ``"/"`` but may be ``"$"`` for agents whose
|
||||
native skills invocation uses dollar-prefixed chat commands.
|
||||
"""
|
||||
return re.sub(
|
||||
r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_]*)__",
|
||||
lambda m: "/speckit" + separator + m.group(1).lower().replace("_", separator),
|
||||
lambda m: prefix
|
||||
+ "speckit"
|
||||
+ separator
|
||||
+ m.group(1).lower().replace("_", separator),
|
||||
content,
|
||||
)
|
||||
|
||||
@@ -838,7 +847,12 @@ class IntegrationBase(ABC):
|
||||
content = CommandRegistrar.rewrite_project_relative_paths(content)
|
||||
|
||||
# 8. Replace __SPECKIT_COMMAND_<NAME>__ with invocation strings
|
||||
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
|
||||
invocation_prefix = get_invocation_prefix(
|
||||
agent_name, invoke_separator == "-"
|
||||
)
|
||||
content = IntegrationBase.resolve_command_refs(
|
||||
content, invoke_separator, invocation_prefix
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
@@ -1520,18 +1534,21 @@ class SkillsIntegration(IntegrationBase):
|
||||
return project_root / folder / subdir
|
||||
|
||||
def build_command_invocation(self, command_name: str, args: str = "") -> str:
|
||||
"""Skills use ``/speckit-<stem>`` (hyphenated directory name)."""
|
||||
"""Build the agent's native invocation for a hyphenated skill name."""
|
||||
stem = command_name
|
||||
if stem.startswith("speckit."):
|
||||
stem = stem[len("speckit."):]
|
||||
|
||||
invocation = "/speckit-" + stem.replace(".", "-")
|
||||
prefix = "$" if is_dollar_skills_agent(self.key, True) else "/"
|
||||
invocation = prefix + "speckit-" + stem.replace(".", "-")
|
||||
if args:
|
||||
invocation = f"{invocation} {args}"
|
||||
return invocation
|
||||
|
||||
@staticmethod
|
||||
def _inject_hook_command_note(content: str) -> str:
|
||||
def _inject_hook_command_note(
|
||||
content: str, invocation_prefix: str = "/"
|
||||
) -> str:
|
||||
"""Insert a dot-to-hyphen note before each hook output instruction.
|
||||
|
||||
Targets the line ``- For each executable hook, output the following``
|
||||
@@ -1540,6 +1557,11 @@ class SkillsIntegration(IntegrationBase):
|
||||
above them.
|
||||
"""
|
||||
note = _HOOK_COMMAND_NOTE.rstrip("\n")
|
||||
if invocation_prefix != "/":
|
||||
note = note.replace(
|
||||
"`/speckit-git-commit`",
|
||||
f"`{invocation_prefix}speckit-git-commit`",
|
||||
)
|
||||
|
||||
def repl(m: re.Match[str]) -> str:
|
||||
indent = m.group(1)
|
||||
@@ -1573,10 +1595,13 @@ class SkillsIntegration(IntegrationBase):
|
||||
Called by external skill generators (presets, extensions) to let
|
||||
the integration inject agent-specific frontmatter or body
|
||||
transformations. The base implementation injects shared skills
|
||||
guidance for converting dotted hook command names to hyphenated
|
||||
slash commands. Subclasses may override — see ``ClaudeIntegration``.
|
||||
guidance for converting dotted hook command names to the agent-native
|
||||
hyphenated command invocation (e.g. ``/speckit-git-commit`` or
|
||||
``$speckit-git-commit``). Subclasses may override -- see
|
||||
``ClaudeIntegration``.
|
||||
"""
|
||||
return self._inject_hook_command_note(content)
|
||||
invocation_prefix = get_invocation_prefix(self.key, True)
|
||||
return self._inject_hook_command_note(content, invocation_prefix)
|
||||
|
||||
def setup(
|
||||
self,
|
||||
|
||||
@@ -59,8 +59,7 @@ class KimiIntegration(SkillsIntegration):
|
||||
|
||||
def post_process_skill_content(self, content: str) -> str:
|
||||
"""Ensure in-skill cross-command references use Kimi's `/skill:` syntax."""
|
||||
content = super().post_process_skill_content(content)
|
||||
return content.replace("/speckit-", "/skill:speckit-")
|
||||
return super().post_process_skill_content(content)
|
||||
|
||||
@classmethod
|
||||
def options(cls) -> list[IntegrationOption]:
|
||||
|
||||
@@ -40,6 +40,7 @@ from .._init_options import (
|
||||
load_init_options,
|
||||
resolve_active_agent_for_registration,
|
||||
)
|
||||
from .._invocation_style import get_invocation_prefix
|
||||
from ..integrations.base import IntegrationBase
|
||||
from .._utils import dump_frontmatter, version_satisfies
|
||||
from ..shared_infra import (
|
||||
@@ -2424,13 +2425,15 @@ class PresetManager:
|
||||
|
||||
Looks up the agent's invoke separator and rewrites each
|
||||
``__SPECKIT_COMMAND_<NAME>__`` placeholder into the matching
|
||||
slash-command invocation — ``/speckit-<cmd>`` for a ``-`` separator,
|
||||
``/speckit.<cmd>`` for ``.`` — the same rendering the command layer
|
||||
applies via ``CommandRegistrar.register_commands()``.
|
||||
agent-native invocation -- ``/speckit-<cmd>`` or ``$speckit-<cmd>`` for
|
||||
a ``-`` separator, ``/speckit.<cmd>`` for ``.``, or
|
||||
``/skill:speckit-<cmd>`` for skill-colon agents (e.g. Kimi) -- the
|
||||
same rendering the command layer applies via
|
||||
``CommandRegistrar.register_commands()``.
|
||||
|
||||
For dual-layout agents (e.g. Bob) the separator depends on the
|
||||
project's persisted skills state, so — when *project_root* is provided
|
||||
— the separator is resolved from the integration via
|
||||
project's persisted skills state, so -- when *project_root* is provided
|
||||
-- the separator is resolved from the integration via
|
||||
``invoke_separator_for_mode`` rather than the single static
|
||||
``AGENT_CONFIGS`` value.
|
||||
"""
|
||||
@@ -2451,7 +2454,8 @@ class PresetManager:
|
||||
separator = registrar.AGENT_CONFIGS.get(selected_ai, {}).get(
|
||||
"invoke_separator", "."
|
||||
)
|
||||
return IntegrationBase.resolve_command_refs(body, separator)
|
||||
prefix = get_invocation_prefix(selected_ai, separator == "-")
|
||||
return IntegrationBase.resolve_command_refs(body, separator, prefix)
|
||||
|
||||
def _build_extension_skill_restore_index(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Index extension-backed skill restore data by skill directory name."""
|
||||
|
||||
@@ -272,27 +272,56 @@ _BASH_FORMAT_COMMAND_RE = re.compile(
|
||||
_POWERSHELL_FORMAT_COMMAND_RE = re.compile(
|
||||
r"Format-SpecKitCommand\s+-CommandName\s+(['\"])([A-Za-z0-9_.-]+)\1(?:\s+-RepoRoot\s+[^\r\n]+)?"
|
||||
)
|
||||
_PYTHON_FORMAT_COMMAND_RETURN_RE = re.compile(
|
||||
r'return f"/speckit\{separator\}\{name\}"'
|
||||
)
|
||||
_BASH_FORMATTER_RETURN_RE = re.compile(
|
||||
r'''printf '/speckit%s%s\\n' "\$separator" "\$command_name"'''
|
||||
)
|
||||
_POWERSHELL_FORMATTER_RETURN_RE = re.compile(
|
||||
r'return "/speckit\$separator\$name"'
|
||||
)
|
||||
|
||||
|
||||
def _format_speckit_command(command_name: str, separator: str) -> str:
|
||||
def _format_speckit_command(
|
||||
command_name: str, separator: str, prefix: str = "/"
|
||||
) -> str:
|
||||
name = command_name.strip().lstrip("/")
|
||||
if name.startswith("speckit."):
|
||||
name = name[len("speckit.") :]
|
||||
elif name.startswith("speckit-"):
|
||||
name = name[len("speckit-") :]
|
||||
name = name.replace(".", separator)
|
||||
return f"/speckit{separator}{name}"
|
||||
return f"{prefix}speckit{separator}{name}"
|
||||
|
||||
|
||||
def _resolve_dynamic_command_refs(content: str, separator: str) -> str:
|
||||
def _resolve_dynamic_command_refs(
|
||||
content: str, separator: str, prefix: str = "/"
|
||||
) -> str:
|
||||
"""Render script runtime command helpers for managed shared infra copies."""
|
||||
|
||||
bash_prefix = r"\$" if prefix == "$" else prefix
|
||||
content = _BASH_FORMAT_COMMAND_RE.sub(
|
||||
lambda match: _format_speckit_command(match.group(2), separator),
|
||||
lambda match: _format_speckit_command(
|
||||
match.group(2), separator, bash_prefix
|
||||
),
|
||||
content,
|
||||
)
|
||||
return _POWERSHELL_FORMAT_COMMAND_RE.sub(
|
||||
lambda match: f"'{_format_speckit_command(match.group(2), separator)}'",
|
||||
content = _POWERSHELL_FORMAT_COMMAND_RE.sub(
|
||||
lambda match: f"'{_format_speckit_command(match.group(2), separator, prefix)}'",
|
||||
content,
|
||||
)
|
||||
content = _BASH_FORMATTER_RETURN_RE.sub(
|
||||
f'''printf '{prefix}speckit%s%s\\\\n' "$separator" "$command_name"''',
|
||||
content,
|
||||
)
|
||||
powershell_prefix = "`$" if prefix == "$" else prefix
|
||||
content = _POWERSHELL_FORMATTER_RETURN_RE.sub(
|
||||
f'return "{powershell_prefix}speckit$separator$name"',
|
||||
content,
|
||||
)
|
||||
return _PYTHON_FORMAT_COMMAND_RETURN_RE.sub(
|
||||
f'return f"{prefix}speckit{{separator}}{{name}}"',
|
||||
content,
|
||||
)
|
||||
|
||||
@@ -305,6 +334,7 @@ def refresh_shared_templates(
|
||||
repo_root: Path,
|
||||
console: Any,
|
||||
invoke_separator: str,
|
||||
invoke_prefix: str = "/",
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
"""Refresh default-sensitive shared templates without touching scripts."""
|
||||
@@ -336,7 +366,9 @@ def refresh_shared_templates(
|
||||
continue
|
||||
|
||||
content = src.read_text(encoding="utf-8")
|
||||
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
|
||||
content = IntegrationBase.resolve_command_refs(
|
||||
content, invoke_separator, invoke_prefix
|
||||
)
|
||||
planned_updates.append((dst, rel, content))
|
||||
|
||||
for dst, rel, content in planned_updates:
|
||||
@@ -363,6 +395,7 @@ def install_shared_infra(
|
||||
console: Any,
|
||||
force: bool = False,
|
||||
invoke_separator: str = ".",
|
||||
invoke_prefix: str = "/",
|
||||
refresh_managed: bool = False,
|
||||
refresh_hint: str | None = None,
|
||||
) -> bool:
|
||||
@@ -516,8 +549,12 @@ def install_shared_infra(
|
||||
if not _ensure_or_bucket_dir(dst_path.parent):
|
||||
continue
|
||||
content = src_path.read_text(encoding="utf-8")
|
||||
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
|
||||
content = _resolve_dynamic_command_refs(content, invoke_separator)
|
||||
content = IntegrationBase.resolve_command_refs(
|
||||
content, invoke_separator, invoke_prefix
|
||||
)
|
||||
content = _resolve_dynamic_command_refs(
|
||||
content, invoke_separator, invoke_prefix
|
||||
)
|
||||
planned_copies.append(
|
||||
(
|
||||
dst_path,
|
||||
@@ -566,7 +603,9 @@ def install_shared_infra(
|
||||
continue
|
||||
|
||||
content = src.read_text(encoding="utf-8")
|
||||
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
|
||||
content = IntegrationBase.resolve_command_refs(
|
||||
content, invoke_separator, invoke_prefix
|
||||
)
|
||||
planned_templates.append((dst, rel, content))
|
||||
|
||||
for dst_path, rel, content, mode in planned_copies:
|
||||
|
||||
Reference in New Issue
Block a user