From 6558f1d99e06c361c3f63d4abca50533e63bbffb Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Sat, 15 Aug 2026 19:00:53 +0500 Subject: [PATCH 1/2] fix(alquimia): render /speckit- invocations for the skills-only agent `alquimia` was the only one of the 19 SkillsIntegration subclasses absent from every set in _invocation_style.py. It installs `.alquimia/skills/speckit-/SKILL.md` and its own `build_command_invocation()` already returns `/speckit-plan`, but `is_slash_skills_agent("alquimia", True)` returned False, so the two callers that consult it emitted the dotted form Alquimia never registers: * HookExecutor._render_hook_invocation -> `/speckit.plan` * `specify init`'s Next Steps panel -> `/speckit.plan` Measured before, for an identical on-disk layout: alquimia -> 2.1 /speckit.constitution <-- wrong droid -> 2.1 /speckit-constitution and after: alquimia -> 2.1 /speckit-constitution droid -> 2.1 /speckit-constitution Added to CONDITIONAL_SLASH_AGENTS rather than ALWAYS_SLASH_AGENTS: it is the conservative choice, matching `claude`, which shares alquimia's `commands_subdir: "skills"`. `specify init` writes `ai_skills: true` for alquimia, so this covers the real path. Co-Authored-By: Claude Opus 5 (1M context) --- src/specify_cli/_invocation_style.py | 1 + .../integrations/test_integration_alquimia.py | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/specify_cli/_invocation_style.py b/src/specify_cli/_invocation_style.py index 5cc7098837..50b9ac5f57 100644 --- a/src/specify_cli/_invocation_style.py +++ b/src/specify_cli/_invocation_style.py @@ -18,6 +18,7 @@ CONDITIONAL_SLASH_AGENTS: frozenset[str] = frozenset( { "agy", + "alquimia", "bob", "claude", "copilot", diff --git a/tests/integrations/test_integration_alquimia.py b/tests/integrations/test_integration_alquimia.py index e8eab8281c..e4612d22b2 100644 --- a/tests/integrations/test_integration_alquimia.py +++ b/tests/integrations/test_integration_alquimia.py @@ -37,6 +37,36 @@ def test_requires_cli_is_true(self): assert integration.config["requires_cli"] is True assert integration.multi_install_safe is True + def test_is_slash_skills_agent(self): + """Alquimia installs `.alquimia/skills/speckit-/SKILL.md`, so the + invocation helper must report the hyphenated form when skills are on. + + It was the only `SkillsIntegration` subclass absent from every set in + `_invocation_style`, so `is_slash_skills_agent` returned False and the + two callers that consult it — `HookExecutor._render_hook_invocation` + and `specify init`'s Next Steps panel — emitted the dotted + `/speckit.` form Alquimia never registers. + """ + from specify_cli._invocation_style import is_slash_skills_agent + + assert is_slash_skills_agent("alquimia", True) is True + # Conditional, not always: with skills disabled the dotted form is + # correct, which is what distinguishes this from an ALWAYS_SLASH agent. + assert is_slash_skills_agent("alquimia", False) is False + + def test_build_command_invocation_matches_the_invocation_helper(self): + """The integration's own renderer and the helper must agree. + + `SkillsIntegration.build_command_invocation` already returned + `/speckit-plan`; only the helper disagreed, which is why the two + outputs diverged for the same on-disk layout. + """ + from specify_cli._invocation_style import is_slash_skills_agent + + integration = get_integration("alquimia") + assert integration.build_command_invocation("plan") == "/speckit-plan" + assert is_slash_skills_agent("alquimia", True) is True + def test_build_exec_args_uses_headless_prompt_flag(self): """Workflow dispatch relies on the inherited ``SkillsIntegration.build_exec_args()`` — pin its argv shape so a From b08b7c0bb9ea2e6bef0fdecd192e2f96e2994947 Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Sun, 30 Aug 2026 21:21:55 +0500 Subject: [PATCH 2/2] test(alquimia): pin the invocation invariants instead of asserting them in prose Addresses review feedback. The inline comment claimed "with skills disabled the dotted form is correct" -- that is wrong. The dotted form is never correct for Alquimia, which registers only `speckit-/SKILL.md`. The classification itself stays conditional, because the reviewer's mechanism does not hold: * `is_slash_skills_agent` returning False is not the last word. The remaining consumer falls through to `integration.build_command_invocation`, and `SkillsIntegration` overrides that to emit the hyphenated form (base.py:1607-1617). Both states render `/speckit-git-commit`. * The `False` argument is unreachable. Both writers of `ai_skills` key off `integration.is_skills_mode(...)` (init.py:777-780, _helpers.py:288-291), and `SkillsIntegration.is_skills_mode` returns True unconditionally (base.py:1564-1570). * Being a `SkillsIntegration` does not imply always-slash: ALWAYS_SLASH (devin, droid, grok, trae, zed) and CONDITIONAL_SLASH (agy, hermes, lingma, vibe, rovodev) are BOTH populated entirely by SkillsIntegration subclasses. Moving Alquimia alone would split it from five identical peers while changing no rendered output. Replaces the incorrect comment with three tests: test_ai_skills_is_always_persisted_for_alquimia test_classified_like_its_skills_only_peers test_never_renders_the_dotted_form_in_either_skills_state The last one asserts the rendered invocation is `/speckit-git-commit` in both skills states, exercising the `build_command_invocation` fallback -- so the reviewer's actual concern is enforced by a test rather than by prose. Co-Authored-By: Claude Opus 5 (1M context) --- .../integrations/test_integration_alquimia.py | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/tests/integrations/test_integration_alquimia.py b/tests/integrations/test_integration_alquimia.py index e4612d22b2..9a299a624f 100644 --- a/tests/integrations/test_integration_alquimia.py +++ b/tests/integrations/test_integration_alquimia.py @@ -50,10 +50,73 @@ def test_is_slash_skills_agent(self): from specify_cli._invocation_style import is_slash_skills_agent assert is_slash_skills_agent("alquimia", True) is True - # Conditional, not always: with skills disabled the dotted form is - # correct, which is what distinguishes this from an ALWAYS_SLASH agent. + # Conditional, not always -- matching every other skills-only agent. + # This is NOT a claim that the dotted form is ever right for Alquimia: + # it never is. It is that the `False` argument is unreachable for a + # skills-only integration, because `ai_skills` is persisted straight + # from `is_skills_mode()`, which `SkillsIntegration` returns + # unconditionally. `test_ai_skills_is_always_persisted_for_alquimia` + # and `test_classified_like_its_skills_only_peers` pin both halves. assert is_slash_skills_agent("alquimia", False) is False + def test_ai_skills_is_always_persisted_for_alquimia(self): + """The conditional resolves to True for every real Alquimia project. + + Both writers of `ai_skills` key off `integration.is_skills_mode(...)` + (`commands/init.py` on init, `integrations/_helpers.py` on + install/use/upgrade), and `SkillsIntegration.is_skills_mode` returns + True unconditionally. So `is_ai_skills_enabled(opts)` is True for any + Alquimia project written by any supported path, and the conditional + classification behaves exactly like an always-slash one. + """ + integration = get_integration("alquimia") + assert integration.is_skills_mode() is True + assert integration.is_skills_mode({}, project_root=None) is True + + def test_classified_like_its_skills_only_peers(self): + """Alquimia must sit in the same set as the other skills-only agents. + + Being a `SkillsIntegration` does not by itself imply always-slash: + `ALWAYS_SLASH_AGENTS` and `CONDITIONAL_SLASH_AGENTS` are *both* full of + `SkillsIntegration` subclasses. What the conditional set actually holds + is the agents whose skills mode is recorded in init options, which is + where Alquimia belongs. Moving it alone to the always set would make it + inconsistent with five identical peers for no behavioural gain. + """ + from specify_cli._invocation_style import ( + ALWAYS_SLASH_AGENTS, + CONDITIONAL_SLASH_AGENTS, + ) + + peers = {"rovodev", "agy", "hermes", "lingma", "vibe"} + assert peers <= CONDITIONAL_SLASH_AGENTS + assert "alquimia" in CONDITIONAL_SLASH_AGENTS + assert "alquimia" not in ALWAYS_SLASH_AGENTS + + def test_never_renders_the_dotted_form_in_either_skills_state(self): + """No reachable path may emit `/speckit.` for Alquimia. + + The helper returning False is not the last word: the remaining consumer + (`_register_extension_skills`'s command-ref resolution) falls through to + `integration.build_command_invocation`, which `SkillsIntegration` + overrides to emit the hyphenated skill form. This pins that fallback so + the dotted form cannot reappear from that direction either. + """ + from specify_cli._invocation_style import ( + is_dollar_skills_agent, + is_slash_skills_agent, + ) + + integration = get_integration("alquimia") + for skills_enabled in (True, False): + if is_dollar_skills_agent("alquimia", skills_enabled): + rendered = "$speckit-git-commit" + elif is_slash_skills_agent("alquimia", skills_enabled): + rendered = "/speckit-git-commit" + else: + rendered = integration.build_command_invocation("speckit.git.commit") + assert rendered == "/speckit-git-commit", skills_enabled + def test_build_command_invocation_matches_the_invocation_helper(self): """The integration's own renderer and the helper must agree.